diff --git a/.github/workflows/jstest.yml b/.github/workflows/jstest.yml new file mode 100644 index 000000000..f5fb70777 --- /dev/null +++ b/.github/workflows/jstest.yml @@ -0,0 +1,19 @@ +name: jstest + +on: [push] + +jobs: + jstest: + runs-on: ubuntu-22.04 + name: jstest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Install dependencies + # npm install, not npm ci: the repo ignores package-lock.json (.gitignore), so + # there is no committed lockfile for ci to read. + run: npm install + - name: Run frontend tests + run: npm test diff --git a/README.rst b/README.rst index 8c9ac1da5..029b5057a 100644 --- a/README.rst +++ b/README.rst @@ -58,3 +58,26 @@ And now you are ready to start the webserver:: Setting the environment variables is needed when developing on a PC. Open your browser and point to http://localhost:8888/. + +Test +---- + +The test suite in ``test/`` contains HTTP-level characterization tests (pytest + tornado's testing tools). +They run against a faked audio backend, so no JACK, mod-host or MOD hardware is needed. + +First complete the Install section above (virtualenv, python requirements and ``make -C utils`` — the tests +import the webserver, which requires ``utils/libmod_utils.so``). + +On Python 3.10 or newer, the pinned tornado 4.3 needs a one-time patch (see the note in ``requirements.txt``):: + + $ sed -i 's/collections.MutableMapping/collections.abc.MutableMapping/' modui-env/lib/python3.*/site-packages/tornado/httputil.py + +Install the test requirements and run the suite from the repository root:: + + $ source modui-env/bin/activate + $ pip3 install -r test-requirements.txt + $ pytest + +NOTE: ``test/hmi-protocol-integrationtest.py`` is not part of this suite — it is a standalone integration +test for the HMI serial protocol that requires JACK, mod-host and a serial device, and pytest does not +collect it. diff --git a/html/css/less/main.less b/html/css/less/main.less index 2e0eed41c..5afd1a9f5 100644 --- a/html/css/less/main.less +++ b/html/css/less/main.less @@ -72,6 +72,7 @@ @import 'cloud_plugins.less'; @import 'banks.less'; @import 'file_manager.less'; +@import 'tone3000.less'; @import 'pedalboards.less'; @import 'nprogress.less'; @import 'addressings.less'; @@ -350,6 +351,19 @@ hr.dotted { background-position: -180px bottom; } } + #mod-tone3000 { + background-image: url(../img/tone3000-icon.png); + background-position: center center; + background-size: 30px 30px; + background-repeat: no-repeat; + transition: all 0.33s; + &:hover { + background-color: #000; + } + &.selected { + background-color: #000; + } + } #banks-saving { margin: 21px 0 0 6px; font-size: 10px; @@ -517,7 +531,8 @@ header#pedalboard-actions, #bank-library header, #cloud-plugins-library header, #pedalboards-library header, -#file-manager-library header { +#file-manager-library header, +#tone3000-library header { display: block; left: 0; height: 45px; diff --git a/html/css/less/tone3000.less b/html/css/less/tone3000.less new file mode 100644 index 000000000..245eb5f33 --- /dev/null +++ b/html/css/less/tone3000.less @@ -0,0 +1,77 @@ +#tone3000-library { + background: #2c2c2c url(../img/watermark.png) 100% 100% no-repeat; + top: 0; + bottom: 46px; + left: 0; + right: 0; + overflow-x: hidden; + overflow-y: auto; + position: absolute; + z-index: 2; +} +/* Nothing to lay out: the catalog, and everything the user does with it, lives in a + popup floated over this panel. All this pane holds is the way in. */ +#tone3000-wrapper { + position: absolute; + top: 45px; + bottom: 46px; + left: 0; + right: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + color: #ccc; +} +#tone3000-logo { + width: 96px; + height: 96px; +} +#tone3000-wrapper h2 { + color: #fff; + font-size: 24px; + font-weight: normal; + text-transform: uppercase; + margin: .8em 0 .2em; +} +#tone3000-wrapper .tone3000-hint { + font-size: 13px; + line-height: 1.5; + color: #999; + max-width: 34em; + margin: 0 0 1em; +} +#tone3000-wrapper .tone3000-hint b { + color: #ccc; + font-weight: normal; +} +#tone3000-wrapper .tone3000-hint:last-of-type { + margin-bottom: 1.5em; +} +#tone3000-wrapper .tone3000-autoopen { + font-size: 12px; + color: #999; + cursor: pointer; + margin-top: 1.5em; +} +#tone3000-wrapper .tone3000-autoopen input { + margin-right: .5em; + vertical-align: middle; + cursor: pointer; +} +#tone3000-wrapper .tone3000-autoopen:hover { + color: #ccc; +} +#tone3000-browse { + background: #883996; + border: none; + border-radius: 3px; + color: #fff; + cursor: pointer; + font-size: 14px; + padding: .7em 1.4em; +} +#tone3000-browse:hover { + background: #9d47ad; +} diff --git a/html/css/main.css b/html/css/main.css index 9b948034e..85746bc1b 100644 --- a/html/css/main.css +++ b/html/css/main.css @@ -1 +1,19 @@ -/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,*:before,*:after{color:#000 !important;text-shadow:none !important;background:transparent !important;box-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#883996;text-decoration:none}a:hover,a:focus{color:#56245f;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:0}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:0;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#999}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{padding:.2em;background-color:#f29446}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#999}.text-primary{color:#883996}a.text-primary:hover,a.text-primary:focus{color:#662b71}.text-success{color:#3c763d}a.text-success:hover,a.text-success:focus{color:#2b542c}.text-info{color:#3a633d}a.text-info:hover,a.text-info:focus{color:#274329}.text-warning{color:#fff}a.text-warning:hover,a.text-warning:focus{color:#e6e6e6}.text-danger{color:#a94442}a.text-danger:hover,a.text-danger:focus{color:#843534}.bg-primary{color:#fff;background-color:#883996}a.bg-primary:hover,a.bg-primary:focus{background-color:#662b71}.bg-success{background-color:#dff0d8}a.bg-success:hover,a.bg-success:focus{background-color:#c1e2b3}.bg-info{background-color:#a6ecb1}a.bg-info:hover,a.bg-info:focus{background-color:#7ce38c}.bg-warning{background-color:#f29446}a.bg-warning:hover,a.bg-warning:focus{background-color:#ef7816}.bg-danger{background-color:#f2dede}a.bg-danger:hover,a.bg-danger:focus{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#999}blockquote footer:before,blockquote small:before,blockquote .small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:""}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:"\00A0 \2014"}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:0}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:0;box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:0}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:100%}}@media (min-width:992px){.container{width:970px}}@media (min-width:1240px){.container{width:1270px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*="col-"]{padding-right:0;padding-left:0}.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1240px){.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}table col[class*="col-"]{position:static;display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#999;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#a6ecb1}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#91e89f}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#f29446}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#f0862e}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none;appearance:none}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);box-shadow:inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{line-height:34px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:30px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:46px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-top:4px \9;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:0}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:0}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm textarea.form-control,.form-group-sm select[multiple].form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:0}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:0}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg textarea.form-control,.form-group-lg select[multiple].form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.33}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback,.input-group-lg+.form-control-feedback,.form-group-lg .form-control+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback,.input-group-sm+.form-control-feedback,.form-group-sm .form-control+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#fff}.has-warning .form-control{border-color:#fff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-warning .input-group-addon{color:#fff;background-color:#f29446;border-color:#fff}.has-warning .form-control-feedback{color:#fff}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;white-space:nowrap;vertical-align:middle;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);opacity:.65;-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:focus,.btn-default.focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#883996;border-color:#773284}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#662b71;border-color:#230f27}.btn-primary:hover{color:#fff;background-color:#662b71;border-color:#4f2157}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#662b71;background-image:none;border-color:#4f2157}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#4f2157;border-color:#230f27}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#883996;border-color:#773284}.btn-primary .badge{color:#883996;background-color:#fff}.btn-success{color:#fff;background-color:#883996;border-color:#773284}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#662b71;border-color:#230f27}.btn-success:hover{color:#fff;background-color:#662b71;border-color:#4f2157}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#662b71;background-image:none;border-color:#4f2157}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#fff;background-color:#4f2157;border-color:#230f27}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#883996;border-color:#773284}.btn-success .badge{color:#883996;background-color:#fff}.btn-info{color:#fff;background-color:#883996;border-color:#773284}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#662b71;border-color:#230f27}.btn-info:hover{color:#fff;background-color:#662b71;border-color:#4f2157}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#662b71;background-image:none;border-color:#4f2157}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#fff;background-color:#4f2157;border-color:#230f27}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#883996;border-color:#773284}.btn-info .badge{color:#883996;background-color:#fff}.btn-warning{color:#fff;background-color:#f29446;border-color:#f0862e}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#ef7816;border-color:#95490a}.btn-warning:hover{color:#fff;background-color:#ef7816;border-color:#d3680f}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ef7816;background-image:none;border-color:#d3680f}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#fff;background-color:#d3680f;border-color:#95490a}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#f29446;border-color:#f0862e}.btn-warning .badge{color:#f29446;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;background-image:none;border-color:#ac2925}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#883996;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#56245f;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:0}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:0}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:0}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height, visibility;transition-property:height, visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid \9;border-right:4px solid transparent;border-left:4px solid transparent}.dropup,.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:0;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#883996;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#999;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid \9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle="buttons"]>.btn input[type="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:0}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:0}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:0}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:0}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:0}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#883996}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:0 0 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:0 0 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:0}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#883996}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:0 0 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:0}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:0}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-right:-15px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left !important}.navbar-right{float:right !important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#999}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:0}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:0}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#883996;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{z-index:2;color:#56245f;background-color:#eee;border-color:#ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:3;color:#fff;cursor:default;background-color:#883996;border-color:#883996}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.33}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:0;border-bottom-left-radius:0}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:0;border-bottom-left-radius:0}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#883996}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#662b71}.label-success{background-color:#883996}.label-success[href]:hover,.label-success[href]:focus{background-color:#662b71}.label-info{background-color:#883996}.label-info[href]:hover,.label-info[href]:focus{background-color:#662b71}.label-warning{background-color:#f29446}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ef7816}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#883996;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:0}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:0;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#883996}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:0}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#3a633d;background-color:#a6ecb1;border-color:#89e689}.alert-info hr{border-top-color:#75e174}.alert-info .alert-link{color:#274329}.alert-warning{color:#fff;background-color:#f29446;border-color:#f0662e}.alert-warning hr{border-top-color:#ef5416}.alert-warning .alert-link{color:#e6e6e6}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:0;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#883996;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#883996}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#883996}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#f29446}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{color:#999;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#999}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#883996;border-color:#883996}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#ddb7e4}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#3a633d;background-color:#a6ecb1}a.list-group-item-info,button.list-group-item-info{color:#3a633d}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#3a633d;background-color:#91e89f}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#3a633d;border-color:#3a633d}.list-group-item-warning{color:#fff;background-color:#f29446}a.list-group-item-warning,button.list-group-item-warning{color:#fff}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#fff;background-color:#f0862e}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#fff;border-color:#fff}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:-1px;border-top-right-radius:-1px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a,.panel-title>small,.panel-title>.small,.panel-title>small>a,.panel-title>.small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:-1px;border-top-right-radius:-1px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-right:15px;padding-left:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-left-radius:-1px;border-top-right-radius:-1px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:-1px;border-top-right-radius:-1px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:-1px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:-1px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:-1px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:-1px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:0}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#444}.panel-default>.panel-heading{color:#999;background-color:#444;border-color:#444}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#444}.panel-default>.panel-heading .badge{color:#444;background-color:#999}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#444}.panel-primary{border-color:#883996}.panel-primary>.panel-heading{color:#fff;background-color:#883996;border-color:#883996}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#883996}.panel-primary>.panel-heading .badge{color:#883996;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#883996}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#89e689}.panel-info>.panel-heading{color:#3a633d;background-color:#a6ecb1;border-color:#89e689}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#89e689}.panel-info>.panel-heading .badge{color:#a6ecb1;background-color:#3a633d}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#89e689}.panel-warning{border-color:#f0662e}.panel-warning>.panel-heading{color:#fff;background-color:#f29446;border-color:#f0662e}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#f0662e}.panel-warning>.panel-heading .badge{color:#f29446;background-color:#fff}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#f0662e}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:0}.well-sm{padding:9px;border-radius:0}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none;appearance:none}.tooltip{position:absolute;z-index:1030;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:12px;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:0}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.clearfix,.dl-horizontal dd,.container,.container-fluid,.row,.form-horizontal .form-group,.btn-toolbar,.btn-group-vertical>.btn-group,.nav,.navbar,.navbar-header,.navbar-collapse,.pager,.panel-body{display:block}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after{display:table;content:" "}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (max-width:767px){.visible-xs-block{display:block !important}}@media (max-width:767px){.visible-xs-inline{display:inline !important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table !important}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block !important}}@media (min-width:992px) and (max-width:1239px){.visible-md{display:block !important}table.visible-md{display:table !important}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:992px) and (max-width:1239px){.visible-md-block{display:block !important}}@media (min-width:992px) and (max-width:1239px){.visible-md-inline{display:inline !important}}@media (min-width:992px) and (max-width:1239px){.visible-md-inline-block{display:inline-block !important}}@media (min-width:1240px){.visible-lg{display:block !important}table.visible-lg{display:table !important}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (min-width:1240px){.visible-lg-block{display:block !important}}@media (min-width:1240px){.visible-lg-inline{display:inline !important}}@media (min-width:1240px){.visible-lg-inline-block{display:inline-block !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1239px){.hidden-md{display:none !important}}@media (min-width:1240px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table !important}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}}#cloud-plugins-library{background:#2c2c2c;bottom:45px;left:0;overflow:auto;position:fixed;right:0;top:0;z-index:2}#cloud-plugins-library h1{color:#fff;display:inline-block;font-size:24px;line-height:35px;margin:0 0 18px 0;padding:0}#cloud-plugins-library header .help{background-image:url(../img/icn-help-16x16.png);background-position:center center;background-repeat:no-repeat;display:inline-block;height:21px;width:21px}#cloud-plugins-library header .close{opacity:1;position:absolute;right:10px;top:16px}#cloud-plugins-library .filter{bottom:45px;left:0;position:fixed;top:45px;right:0;width:240px;overflow:auto}#cloud-plugins-library .filter h1{font-weight:2em;text-transform:uppercase;font-weight:bold;margin:0 0 0 10px}#cloud-plugins-library .filter h2{color:white;font-size:15px;text-transform:uppercase;font-weight:bold;margin-top:1em;margin-left:10px}#cloud-plugins-library .filter .control-group .switch{margin:5px 0 5px 15px;position:relative;width:18rem;height:2.5rem;font-size:0}#cloud-plugins-library .filter .control-group .switch input{position:absolute;top:0;z-index:2;opacity:0;cursor:pointer;height:2.5rem;width:5rem;left:5.5rem;margin:0}#cloud-plugins-library .filter .control-group .switch input:checked{z-index:1}#cloud-plugins-library .filter .control-group .switch input:checked+label{opacity:1;cursor:default}#cloud-plugins-library .filter .control-group .switch input:checked~.toggle-outside .toggle-inside{left:.25rem;background:#883996}#cloud-plugins-library .filter .control-group .switch input:not(:checked)+label:hover{opacity:.5}#cloud-plugins-library .filter .control-group .switch input~input:checked~.toggle-outside .toggle-inside{left:2.65rem;background:#4a4a4a}#cloud-plugins-library .filter .control-group .switch label{color:#fff;opacity:.33;transition:opacity .25s ease;cursor:pointer;font-size:14px;line-height:3rem;display:inline-block;width:5rem;height:100%;margin:0;text-align:center}#cloud-plugins-library .filter .control-group .switch label:first-of-type{text-align:left}#cloud-plugins-library .filter .control-group .switch label:last-of-type{margin-left:5.5rem}#cloud-plugins-library .filter .control-group .switch .toggle-outside{height:100%;border-radius:2rem;padding:.25rem;overflow:hidden;transition:.25s ease all;background:#fff;opacity:.8;position:absolute;width:5rem;left:5.5rem}#cloud-plugins-library .filter .control-group .switch .toggle-inside{border-radius:5rem;background:#4a4a4a;position:absolute;transition:.25s ease all;height:2rem;width:2rem}#cloud-plugins-library .filter .control-group select{width:50px}#cloud-plugins-library .filter .control-group input[type='checkbox']{display:none}#cloud-plugins-library .filter .control-group input[type='checkbox']+label{cursor:pointer;background:#222;color:#fff;display:block;height:32px;line-height:32px;width:100%;margin-bottom:1px;padding-left:15px;border:0 solid #f29446}#cloud-plugins-library .filter .control-group input[type='checkbox']+label:hover{background:#000}#cloud-plugins-library .filter .control-group input[type='checkbox']:checked+label{background:#f29446}#cloud-plugins-library .filter .control-group input[type='checkbox']:checked+label:hover{background:#600e6f}#cloud-plugins-library .filter>.form-group .input-clean{margin-right:8px !important}#cloud-plugins-library .filter .categories{width:100%;margin:0 0 15px 0}#cloud-plugins-library .filter .categories li{display:block;background:#222;color:#888;cursor:pointer;font-weight:bold;margin:0 0 1px 0;padding:0 15px;position:relative;transition:all .33s;line-height:32px;height:32px}#cloud-plugins-library .filter .categories li span{float:right}#cloud-plugins-library .filter .categories li.selected{background:#883996;color:#fff}#cloud-plugins-library .filter .categories li:hover{background:#000}#cloud-plugins-library .filter .categories li.selected:hover{background:#600e6f}.cloud-plugins{left:240px;position:fixed;right:0;top:45px;bottom:45px;background:#111 url(../img/watermark.png) 100% 100% no-repeat;overflow-y:auto}.cloud-plugins .plugins-wrapper{width:100%}.cloud-plugins .cloud-plugin{color:#fff;float:left;width:100%;position:relative;min-height:1px;padding-right:15px;padding-left:15px;height:120px;border:5px solid #000;padding:0 !important;background:rgba(255,255,255,0.07);transition:all .33s;cursor:pointer}@media (min-width:768px){.cloud-plugins .cloud-plugin{float:left;width:50%}}@media (min-width:992px){.cloud-plugins .cloud-plugin{float:left;width:50%}}@media (min-width:1240px){.cloud-plugins .cloud-plugin{float:left;width:33.33333333%}}.cloud-plugins .cloud-plugin:hover{background-color:black}.cloud-plugins .cloud-plugin:hover .description{background:rgba(0,0,0,0.8)}.cloud-plugins .cloud-plugin:hover .description hr{border-color:#f29446}.cloud-plugins .cloud-plugin:hover figure{top:-5px}.cloud-plugins .cloud-plugin:hover div.demo-plugin{opacity:0}.cloud-plugins .cloud-plugin:hover span.price{position:absolute;top:25px;margin-left:98px;padding:3px 0;transform:rotate(0deg) scale(1.3);width:80px}.cloud-plugins .cloud-plugin:hover span.licensed{position:absolute;top:25px;margin-left:98px;padding:3px 0;transform:rotate(0deg) scale(1.3);width:113px;text-align:center}.cloud-plugins .cloud-plugin:hover span.coming-soon{position:absolute;top:25px;margin-left:98px;padding:3px 0;transform:rotate(0deg) scale(1.3);width:113px;text-align:center}.cloud-plugins .cloud-plugin:hover span.price-container{margin-left:170px;width:200px}.cloud-plugins .cloud-plugin figure{height:100px;position:absolute;top:10px;left:20px;margin:0;transition:all .33s linear;display:flex;flex-direction:column;justify-content:center}.cloud-plugins .cloud-plugin figure img{height:auto;margin:0 auto 3px;width:auto;pointer-events:none;max-width:256px;max-height:64px}.cloud-plugins .cloud-plugin span.price{transition:all .33s linear}.cloud-plugins .cloud-plugin span.price-container{transition:all .33s linear}.cloud-plugins .cloud-plugin .cloud-plugin-border{border:1px solid #333;width:100%;height:100%}.cloud-plugins .cloud-plugin .description{transition:all .33s;display:block;line-height:1.5;overflow:hidden;position:absolute;top:1px;right:1px;bottom:1px;left:80px;background:rgba(33,33,33,0.66)}.cloud-plugins .cloud-plugin .description .no_description{color:rgba(255,255,255,0.25)}.cloud-plugins .cloud-plugin .description hr{transition:all .33s;border:none;border-bottom:1px solid #883996;margin:0;position:absolute;top:50%;left:0;width:calc(100% - 5px)}.cloud-plugins .cloud-plugin .description .title{display:block;text-transform:uppercase;font-weight:bold;overflow:hidden;padding:8px 10px 1px 10px}.cloud-plugins .cloud-plugin .description .author{display:block;padding:1px 10px}.cloud-plugins .cloud-plugin .description p{position:absolute;left:10px;right:10px;top:75%;transform:translateY(-50%);height:2.4em;line-height:1.25em;overflow:hidden}.cloud-plugins .cloud-plugin .actions{font-size:11px;font-weight:bold;text-transform:uppercase}.cloud-plugins .cloud-plugin .actions>span{cursor:pointer}.cloud-plugins .cloud-plugin .actions>span:hover{text-decoration:underline}.cloud-plugins .cloud-plugin .actions span.duplicate{margin:0 6px}.cloud-plugins h2{margin-left:23px;font-size:20px;color:#ccc}.cloud-plugins h3{color:#ccc}.cloud-plugins .featured-plugins{position:relative;height:360px;width:100%;display:block}.cloud-plugins .featured-plugins.double,.cloud-plugins .featured-plugins.single{height:490px !important}.cloud-plugins .featured-plugins.double .box,.cloud-plugins .featured-plugins.single .box{transition:all 0s !important}.cloud-plugins .featured-plugins.double .carousel,.cloud-plugins .featured-plugins.single .carousel{height:485px !important}.cloud-plugins .featured-plugins.double .carousel .slick-list,.cloud-plugins .featured-plugins.single .carousel .slick-list{height:485px !important}.cloud-plugins .featured-plugins.double .box{margin:30px 80px !important}.cloud-plugins .featured-plugins.single .box{margin:30px 30px !important}.cloud-plugins .featured-plugins .carousel{position:absolute;top:0;bottom:0;right:90px;left:90px;height:380px !important}.cloud-plugins .featured-plugins .carousel .slick-list{height:380px !important}.cloud-plugins .featured-plugins .carousel .featured{cursor:pointer;margin-top:40px}.cloud-plugins .featured-plugins .carousel .featured .box{padding:4px;border:1px solid #737373;height:300px;opacity:.4;margin:0 24px}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box{border:1px solid #444;text-align:center;height:290px}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .price-container{position:absolute;top:0;left:0;width:90px;height:90px;overflow:hidden;color:#fff}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .price{background-color:#883996;border:1px solid #883996;position:absolute;top:9px;left:-30px;padding:3px 0;transform:rotate(-45deg);min-width:108px;text-align:center}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .price:before{content:"€ "}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .licensed{background-color:#883996;border:1px solid #883996;position:absolute;top:16px;left:-30px;padding:3px 27px;font-size:12px;transform:rotate(-45deg)}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .licensed:before{content:"PURCHASED"}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box h2{color:#c3c3c3;text-align:center}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .img-container{height:113px;width:100%}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .img-container img{vertical-align:middle;transition:all 500ms ease;width:auto;height:auto;margin:auto;display:block;max-height:113px;padding:0 20px;max-width:100%}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box p{color:#e0e0e0;padding:5px 5px 0 5px;height:59px}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box button{background-color:transparent;color:#ff3838;border:1px solid #ff3838;text-transform:uppercase;padding:8px 20px}.cloud-plugins .featured-plugins .carousel .featured.slick-center{height:390px}.cloud-plugins .featured-plugins .carousel .featured.slick-center .box{opacity:1;transform:scale(1.2) translate(0, 0);transition:all 500ms ease}.cloud-plugins .featured-plugins .carousel .slick-arrow{width:60px;height:60px;top:200px}.cloud-plugins .featured-plugins .carousel .slick-arrow:before{font-size:60px}.cloud-plugins .featured-plugins .carousel .slick-prev{left:-70px !important}.cloud-plugins .featured-plugins .carousel .slick-next{right:-70px !important}.plugin-container .status-container{position:absolute;top:0;left:0;right:2px;text-align:right;padding-right:2px}.plugin-container .status{padding:1px 4px;text-align:center;text-transform:uppercase;font-weight:bold;font-size:9px;margin-left:1px}.plugin-container .status.installed{border:1px solid #883996;background-color:#883996}.plugin-container .status.installed:after{content:"installed"}.plugin-container .status.outdated{border:1px solid #f29446;background-color:#f29446;color:#212121}.plugin-container .status.outdated:after{content:"new version"}.plugin-container .price-container{position:absolute;top:0;left:0;width:90px;height:90px;overflow:hidden}.plugin-container .price{background-color:#883996;border:1px solid #883996;position:absolute;top:9px;left:-25px;padding:3px 0;transform:rotate(-45deg);min-width:108px;text-align:center}.plugin-container .price:before{content:"€ "}.plugin-container .licensed{background-color:#883996;border:1px solid #883996;position:absolute;top:21px;left:-30px;padding:3px 27px;font-size:14px;transform:rotate(-45deg)}.plugin-container .licensed:before{content:"PURCHASED"}.plugin-container .coming-soon{background-color:#883996;border:1px solid #883996;position:absolute;top:26px;left:-35px;padding:3px 27px;font-size:12px;transform:rotate(-45deg);width:135px}.plugin-container .coming-soon:before{content:"TRIAL ONLY"}.plugin-container .status.buildEnvironment{border:1px solid #ff2927;background-color:#ff2927}.plugin-container .status.buildEnvironment:after{content:"local"}.plugin-container .status.buildEnvironment.labs:after{content:"labs"}.plugin-container .status.buildEnvironment.dev:after{content:"dev"}.plugin-container .status.buildEnvironment.prod{display:none}.plugin-container .status.buildEnvironment.prod:after{content:""}.plugin-container .demo{border:1px solid #ff2927;background-color:#ff2927}.plugin-container .demo:after{content:"demo"}.plugin-container .description-price-container{position:absolute;top:-17px;left:-17px;overflow:hidden;height:200px}.plugin-container .description-price-container .description-price{background-color:#883996;border:1px solid #883996;position:relative;top:34px;left:-57px;padding:3px 0;transform:rotate(-45deg);min-width:250px;text-align:center;font-size:34px;z-index:11}.plugin-container .description-price-container .description-price:before{content:"€ "}.plugin-container .description-price-container .description-licensed{background-color:#883996;border:1px solid #883996;position:relative;top:50px;left:-60px;padding:3px 0;transform:rotate(-45deg);min-width:280px;text-align:center;font-size:34px;z-index:1}.plugin-container .description-price-container .description-licensed:before{content:"PURCHASED"}.plugin-container .description-price-container .description-coming-soon{background-color:#883996;border:1px solid #883996;position:relative;top:50px;left:-70px;padding:3px 0;transform:rotate(-45deg);min-width:280px;text-align:center;font-size:26px;z-index:1}.plugin-container .description-price-container .description-coming-soon:before{content:"TRIAL ONLY"}.buy-button{margin-top:-20px;position:relative}.buy-button iframe{position:absolute;top:-17px;left:0;min-width:115px;z-index:1}.shopify-fake-button{position:absolute;top:3px;left:0;z-index:0;background-color:#c224b0;font-family:Arial;font-size:12px;opacity:.25;min-width:115px;min-height:34px;padding:9px 15px;text-align:center}#bank-library{bottom:45px;left:0;overflow-x:hidden;overflow-y:auto;position:absolute;right:0;top:0;z-index:2}#bank-library .ui-draggable-dragging{cursor:webkit-dragging !important}#bank-library .mod-banks-drag-item{background:#f29446;color:white;font-weight:bold;z-index:10000;box-shadow:0 5px 15px black;opacity:.5}#bank-library .mod-banks-drag-item .img{display:block;height:auto;margin:0 auto;text-align:center;width:100%;background:url(../img/background.jpg);background-size:auto 100%;background-position:center center;box-shadow:inset 0 3px 6px black;border:1px solid #000}#bank-library .mod-banks-drag-item .title{display:block;line-height:32px;font-weight:bold;overflow:hidden;position:relative;padding-left:5px}#bank-library span.move{background-image:url(../img/move.png);background-position:left center;background-repeat:no-repeat;display:block;float:left;height:32px;left:0;cursor:move !important;margin:-1px 6px 0 0;width:24px}#bank-library span.remove{cursor:pointer;float:right;width:32px;height:32px;display:block;background-image:url(../img/icons/25/delete.png);background-repeat:no-repeat;background-position:50% 50%;transition:background-color .33s}#bank-library span.remove:hover{background-color:#f29446}#bank-library span.title{display:block;float:left;height:32px;overflow:hidden;max-width:70%;white-space:nowrap}#bank-library input::selection{background:#883996 !important;color:white}#bank-library .icon-add{width:1em;height:1em;display:inline-block;font-size:inherit;vertical-align:baseline}#bank-library .icon-add i{position:absolute;width:1em;margin-top:.3em;border-top:.4em solid #888}#bank-library .icon-add b{position:absolute;height:1em;margin-left:.3em;border-left:.4em solid #888}#bank-library #bank-list{left:0;list-style:none;margin:0;padding:0;position:fixed;bottom:45px;overflow-y:auto;top:45px;width:20%;background:#2c2c2c}#bank-library #bank-list>div.add-bank{color:#fff;cursor:pointer;padding-left:5px;line-height:32px;text-transform:uppercase;transition:all .33s}#bank-library #bank-list>div.add-bank:hover{background:#f29446}#bank-library #bank-list>div.add-bank .icon-add{display:inline-block;width:24px;margin-right:6px;padding-left:5px;box-sizing:border-box}#bank-library #bank-list>div.selected span.move{background-position:right center}#bank-library #bank-list>div.selected span.remove{background-position:right center}#bank-library #bank-list .edit-bank{color:#555;width:100% !important;height:30px;margin:1px;padding-left:.5em;background-color:#fff;background-image:none;border:0 solid #ccc;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out}#bank-library #bank-list div{background:#222;color:#888;cursor:pointer;font-weight:bold;margin:0 0 1px 0;content:\002B;position:relative;transition:all .33s}#bank-library #bank-list div.selected{background:#883996;color:#fff}#bank-library #bank-list div:hover{background:#000}#bank-library #bank-list div.selected:hover{background:#600e6f}#bank-library #bank-list .bank-item{display:block;background:#222;color:#888;line-height:32px;height:32px;cursor:pointer;font-weight:bold;margin:0 0 1px 0;padding-left:5px;position:relative}#bank-library #bank-list .bank-item .remove{border-radius:0}#bank-library #bank-list .filter_{border-color:#111;border-style:dotted;border-width:0 1px 0 0;bottom:46px;left:0;padding-top:18px;position:fixed;top:0;right:0;width:240px;overflow:auto}#bank-library #bank-list .filter_ h1{font-weight:2em;text-transform:uppercase;font-weight:bold;margin:0 0 0 10px}#bank-library #bank-list .filter_ h2{color:white;font-size:15px;text-transform:uppercase;font-weight:bold;margin-top:1em;margin-left:10px}#bank-library #bank-list .filter_ .control-group{float:left;margin-right:9px;margin-left:10px}#bank-library #bank-list .filter_ .control-group label{color:#fff;display:inline-block;font-size:12px;font-weight:bold}#bank-library #bank-list .filter_ .control-group select{width:50px}#bank-library #bank-list .filter_>.form-group{padding:15px}#bank-library #bank-title{background:#111;left:20%;height:92px;margin:0;padding:0;position:fixed;text-align:left;top:45px;width:50%}#bank-library #bank-title h1{font-size:2em;padding:0;max-height:2em;display:block;margin:.5em .8em 0 .8em;padding-bottom:.2em;color:white;white-space:nowrap;overflow:hidden}#bank-library #bank-title form{margin:0 2em;color:#666}#bank-library #bank-title h2{margin:4px 0 0 14px;display:block;text-transform:initial !Important;color:white;font-size:12px;line-height:20px;text-align:left}#bank-library #bank-title small{font-size:10px;color:#bbb;text-align:left;display:block;margin-left:14px}#bank-library #bank-title select{display:block;float:left;height:22px;margin:5px 2.5%;width:20%;font-size:10px}#bank-library #bank-pedalboards{background:#111;bottom:46px;left:20%;list-style:none;margin:0;overflow-x:hidden;overflow-y:auto;padding:0 0 200px 0;position:fixed;top:164px;width:50%}#bank-library #bank-pedalboards>div{margin:0 10px 20px 20px}#bank-library #bank-pedalboards .pedalboard{color:#fff;cursor:-webkit-grab !important;cursor:grab !important;width:100%;transition:all .33s}#bank-library #bank-pedalboards .pedalboard:active{cursor:-webkit-grabbing !important;cursor:grabbing !important}#bank-library #bank-pedalboards .pedalboard:hover{background-color:#883996}#bank-library #bank-pedalboards .pedalboard .info{height:32px;padding-left:5px;background:rgba(255,255,255,0.07)}#bank-library #bank-pedalboards .pedalboard .title{display:block;line-height:32px;font-weight:bold;overflow:hidden;position:relative}#bank-library #bank-pedalboards .pedalboard .img{display:inline-block;float:left;height:auto;margin:0 9px 0 0;text-align:center;width:100%;background:url(../img/background.jpg);background-size:auto 100%;background-position:center center;box-shadow:inset 0 3px 6px black;border:1px solid #000}#bank-library #bank-pedalboards .mode{border:none;margin:22px 0 18px 0;text-align:center}#bank-library #bank-edit{position:fixed;display:block;background:#111 url(../img/watermark.png) 100% 100% no-repeat;width:80%;bottom:45px;top:45px;left:20%}#bank-library #bank-pedalboards-search{bottom:45px;right:0;overflow:auto;position:fixed;top:0;width:30%;z-index:6}#bank-library #bank-pedalboards-search h2{text-transform:uppercase;color:white;font-size:1.5em;font-weight:bold;float:left}#bank-library #bank-pedalboards-search .box{background:#111;padding:0;box-sizing:border-box}#bank-library #bank-pedalboards-search input{width:50%;padding-left:1em}#bank-library #bank-pedalboards-search .remove{display:none !important}#bank-library #bank-pedalboards-search label{color:#fff;font-weight:bold;text-transform:uppercase;float:left;line-height:45px;padding-left:10px}#bank-library #bank-pedalboards-search .control-group{margin-bottom:10px;padding:0 2%;position:relative;width:29.3%}#bank-library #bank-pedalboards-search input[type="search"]{width:50% !important;float:right;margin:5px}#bank-library #bank-pedalboards-search select{width:100%}#bank-library #bank-pedalboards-search .input-clean{background:#333;color:#fff;cursor:pointer;font-size:8px;height:18px;left:auto !important;position:absolute;right:8px !important;text-align:center;text-transform:uppercase;top:31px !important;width:18px;border-radius:100%}#bank-pedalboards-mode{background:#333;left:30%;height:30px;margin:0;padding:20px 0 0 0;position:absolute;text-align:center;top:68px;width:45%}#bank-pedalboards-mode .grid{background:url(../img/icn-grid-list.png) no-repeat left bottom;display:inline-block;height:14px;width:14px}#bank-pedalboards-mode .grid.selected{background-position:left top}#bank-pedalboards-mode .list{background:url(../img/icn-grid-list.png) no-repeat right bottom;display:inline-block;height:14px;width:14px}#bank-pedalboards-mode .list.selected{background-position:right top}.pedalboard .addressings{width:100%}.pedalboard .addressings .footswitch{background:#2b2b2b url(../img/footswitch-free.png) no-repeat center top;float:left;height:20px;width:25%}.pedalboard .addressings .footswitch.addressed{background:#2b2b2b url(../img/footswitch-addressed.png) no-repeat center top}#bank-pedalboards-result{position:fixed;bottom:45px;top:45px;overflow:auto;width:30%;background:#111}#bank-pedalboards-result .pedalboard{color:#fff;background:rgba(255,255,255,0.07);cursor:-webkit-grab;cursor:grab;margin:0 20px 20px 10px;transition:all .33s}#bank-pedalboards-result .pedalboard:active{cursor:-webkit-grabbing !important;cursor:grabbing !important}#bank-pedalboards-result .pedalboard:hover{background-color:#883996}#bank-pedalboards-result .pedalboard .img{display:block;height:auto;margin:0 auto;text-align:center;width:100%;background:url(../img/background.jpg);background-size:auto 100%;background-position:center center;box-shadow:inset 0 3px 6px black;border:1px solid #000}#bank-pedalboards-result .pedalboard .img img{max-width:100%}#bank-pedalboards-result .pedalboard .title{display:block;line-height:32px;font-weight:bold;overflow:hidden;position:relative;padding-left:5px}#bank-pedalboards-result .pedalboard .description{display:block;font-size:11px;height:27px;line-height:1.5;overflow:hidden}#file-manager-library{background:#2c2c2c url(../img/watermark.png) 100% 100% no-repeat;top:0;bottom:46px;left:0;right:0;overflow-x:hidden;overflow-y:auto;position:absolute;z-index:2}#file-manager-wrapper{display:flex;position:fixed;top:45px;bottom:55px;left:2em;right:2em;overflow:auto;padding-top:1%}#file-manager-wrapper iframe{flex:1;border:none}#pedalboards-library{background:#2c2c2c;bottom:45px;left:0;overflow-x:hidden;overflow-y:auto;position:absolute;right:0;top:0;z-index:2}#pedalboards-library h1{color:#fff;display:inline-block;font-size:24px;line-height:35px;margin:0 0 18px 0;padding:0}#pedalboards-library header .form-group{display:inline-block;float:right;margin-top:5px;margin-right:5px}#pedalboards-library header .form-group .form-control{width:200px !important}#pedalboards-library header .form-group .input-clean{line-height:20px;margin-right:12px !important}#pedalboards-library header .view-modes{float:right;margin-right:12px;cursor:pointer}#pedalboards-library header .view-modes .view-mode{font-size:130%}#pedalboards-library header .view-modes .view-mode.selected{color:#fff}#pedalboards-library .filter{border-color:#111;border-style:dotted;border-width:0 1px 0 0;bottom:45px;left:0;position:fixed;right:0;top:45px;width:240px}#pedalboards-library .filter .control-group{float:left;margin-right:9px}#pedalboards-library .filter .control-group label{color:#fff;display:inline-block;font-size:12px;font-weight:bold;width:165px}#pedalboards-library .filter .control-group select{width:50px}#pedalboards-library .pedalboards{left:0;position:fixed;right:0;top:45px;bottom:45px;overflow:auto;background:#111 url(../img/watermark.png) 100% 100% no-repeat;padding-top:1%}#pedalboards-library .pedalboards .pedalboard{color:#fff;float:left;margin:0 0 1% 1%;width:18.8%;position:relative;background-color:transparent;background-image:url(../img/background.jpg);background-position:center center;background-repeat:repeat;background-size:auto 100%;transition:all .33s;cursor:pointer}#pedalboards-library .pedalboards .pedalboard:hover .info{background-color:#883996}#pedalboards-library .pedalboards .pedalboard .img{box-shadow:inset 0 3px 6px black;border:1px solid #000;display:block;width:100%;height:10vw;overflow:hidden;text-align:center;background-image:url(../img/loading-pedalboard.gif);background-position:center center;background-repeat:no-repeat;background-size:auto;position:relative}#pedalboards-library .pedalboards .pedalboard .img.loaded{background-size:contain}#pedalboards-library .pedalboards .pedalboard .img.broken{background-image:url(../img/icons/broken_image.svg);background-size:auto 50%}#pedalboards-library .pedalboards .pedalboard.broken .img::after{content:"";position:absolute;top:0;left:0;right:0;bottom:0;z-index:10;background-color:rgba(17,17,17,0.5);background-image:url(../img/icons/broken_pedal.svg);background-size:auto 50%;background-repeat:no-repeat;background-position:center}#pedalboards-library .pedalboards .pedalboard .info{background-color:#2c2c2c;transition:background-color .33s}#pedalboards-library .pedalboards .pedalboard .title{display:block;line-height:32px;font-weight:bold;overflow:hidden;position:relative;height:32px;left:5px;width:calc(100% - 32px);text-overflow:ellipsis;white-space:nowrap}#pedalboards-library .pedalboards .pedalboard .description{display:block;font-size:12px;height:36px;line-height:1.5;overflow:hidden}#pedalboards-library .pedalboards .pedalboard .actions{font-weight:bold;text-transform:uppercase;position:absolute;top:0;right:0}#pedalboards-library .pedalboards .pedalboard .actions .js-remove{width:32px;height:32px;background-image:url(../img/icons/25/delete.png);background-repeat:no-repeat;background-position:50% 50%;transition:background-color .33s}#pedalboards-library .pedalboards .pedalboard .actions .js-remove:hover{background-color:#f29446}#pedalboards-library .pedalboards .pedalboard .actions>span{cursor:pointer}#pedalboards-library .pedalboards .pedalboard .actions>span:hover{text-decoration:underline}#pedalboards-library .pedalboards .pedalboard .actions>span.duplicate{margin:0 6px}#pedalboards-library .pedalboards.list-selected .img{display:none}#nprogress{pointer-events:none;z-index:103100}#nprogress .bar{background:#883996;position:fixed;z-index:103100;top:45px;left:0;opacity:.5;width:100%;height:2px}#nprogress .peg{display:none}#nprogress .spinner{display:none;position:fixed;z-index:1031;top:45px;right:0;background:rgba(0,0,0,0.5);padding:6px;box-sizing:border-box;transform:translate(-50% -50%)}#nprogress .spinner-icon{width:32px;height:32px;box-sizing:border-box;border:solid 3px transparent;border-top-color:#f29446;border-left-color:rgba(242,148,70,0.66);border-bottom-color:rgba(242,148,70,0.33);border-radius:50%;-webkit-animation:nprogress-spinner 400ms linear infinite;animation:nprogress-spinner 400ms linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .spinner,.nprogress-custom-parent #nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.mod-pedal-settings .mod-address.addressed,.mod-pedal-settings .preset-btn-assign-all.addressed{background-color:#a144b2}.mod-pedal-settings-address .mod-box .form-container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.mod-pedal-settings-address .mod-box .form-container .btn.js-type,.mod-pedal-settings-address .mod-box .form-container .btn.advanced-toggle{border:solid #333 1px}.mod-pedal-settings-address .mod-box .form-container .main-form-container{width:691px;padding-right:3px}.mod-pedal-settings-address .mod-box .form-container .main-form-container label{text-align:center}.mod-pedal-settings-address .mod-box .form-container .main-form-container .btn.js-type.selected{background-color:#333;color:white}.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike{display:flex;flex-basis:100%;align-items:center;color:black;margin:6px 0}.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike:before,.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike:after{content:"";flex-grow:1;background:black;height:3px;font-size:0;line-height:0}.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike:before{margin-right:8px}.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike:after{margin-left:8px}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table{width:100%}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:first-child{background-color:#f66}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(2){background-color:#ff6}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(3){background-color:#6ff}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(4){background-color:#bb7cd5}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(5){background-color:#f5a77e}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(6){background-color:#6f6}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(7){background-color:#f6f}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(8){background-color:#8080ff}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th,.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table td{border:solid #333 1px;text-align:center}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table td{padding:5px;cursor:pointer}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table td.selected{background-color:#883996;color:white;box-shadow:3px 3px 3px #999}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table td.disabled{cursor:auto;color:#999;background-color:#d9d9d9;box-shadow:inset 0 0 3px #555}.mod-pedal-settings-address .mod-box .form-container .main-form-container .midi-custom-uri{color:#883996}.mod-pedal-settings-address .mod-box .form-container .buttons-container{padding-right:3px;padding-bottom:18px}.mod-pedal-settings-address .mod-box .form-container .advanced-container{border-left:1px solid #eee;padding-left:12px;margin-left:12px}.mod-pedal-settings-address .mod-box .form-container .advanced-container .cv-op-mode label{width:100% !important}.mod-pedal-settings-address .mod-box .form-container .advanced-container .cv-op-mode .mod-controls{width:155px !important}.mod-cv-output .output-cv-checkbox{display:flex;position:absolute;top:45px;left:15px}.mod-cv-output .output-cv-checkbox .checkbox-container{position:relative;padding-left:45px;cursor:pointer;font-size:22px;height:35px;width:35px}.mod-cv-output .output-cv-checkbox .checkbox-container .checkmark{position:absolute;top:0;left:0;height:35px;width:35px;background-color:#eee}.mod-cv-output .output-cv-checkbox .checkbox-container .checkmark:after{content:"";position:absolute;display:none;left:13px;top:7px;width:10px;height:17px;border:solid white;border-width:0 4px 4px 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]{position:absolute;opacity:0;cursor:pointer;z-index:5;width:100%;height:100%;top:0;left:0}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]:disabled{cursor:not-allowed}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]:checked~.checkmark{background-color:#883996}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]:checked~.checkmark:after{display:block}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]:disabled~.checkmark{background-color:#999}.mod-cv-output .output-cv-checkbox input[type=text]{font-size:20px}.mod-cv-output .output-cv-checkbox ::selection{background:#883996 !important;color:white !important}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-semibold-webfont.eot');src:url('../fonts/cooper/cooperhewitt-semibold-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-semibold-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-semibold-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-semibold-webfont.ttf') format('truetype');font-weight:600;font-style:normal;font-display:swap}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-light-webfont.eot');src:url('../fonts/cooper/cooperhewitt-light-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-light-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-light-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-light-webfont.ttf') format('truetype');font-weight:300;font-style:normal;font-display:swap}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-lightitalic-webfont.eot');src:url('../fonts/cooper/cooperhewitt-lightitalic-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-lightitalic-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-lightitalic-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-lightitalic-webfont.ttf') format('truetype');font-weight:300;font-style:italic;font-display:swap}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-bookitalic-webfont.eot');src:url('../fonts/cooper/cooperhewitt-bookitalic-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-bookitalic-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-bookitalic-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-bookitalic-webfont.ttf') format('truetype');font-weight:400;font-style:italic;font-display:swap}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-book-webfont.eot');src:url('../fonts/cooper/cooperhewitt-book-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-book-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-book-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-book-webfont.ttf') format('truetype');font-weight:400;font-style:normal;font-display:swap}*{outline:0 !important}html,body{height:100%;overflow:hidden}body{background:#111;min-width:1024px}body.ios{position:fixed;min-width:980px}*{font-family:"cooper hewitt",Sans-serif !important}.unpadded{padding:0 !important}.unpadded-r{padding:0 0 0 7px}.unpadded-l{padding:0 7px 0 0}.ui-draggable:active{cursor:-webkit-grabbing !important;cursor:grabbing !important}.form-control{width:100% !important}#wrapper{bottom:0;left:0;min-width:980px;overflow:hidden;position:absolute;right:0;top:0}hr.dotted{border:1px dotted #444;margin:2px 0 3px 0}.preset-list{width:100%;height:200px !important}.input-clean{background:#333;color:#fff;cursor:pointer;height:18px;left:auto !important;position:absolute;right:0;margin-right:18px !important;text-align:center;text-transform:uppercase;margin-top:-26px !important;width:18px;border-radius:100%}.input-clean:after{content:"×";content:"\00d7";font-size:14px;font-weight:bold}.form-control::selection{background:#883996 !important;color:white !important}#notifications{position:absolute;top:55px;right:0;width:320px;z-index:100000}#notifications>section{border-radius:0;margin-bottom:0 !important;opacity:.96;cursor:pointer}#notifications .progressbar{border:0;height:8px;overflow:hidden;position:absolute;left:8px;bottom:0;width:306px}#notifications .progressbar .progressbar-value{background-color:rgba(136,57,150,0.7);height:90%;margin:0}#main-menu{background:#111;bottom:0;color:#fff;height:45px;left:0;position:absolute;right:0;z-index:10}#main-menu>div{cursor:pointer;float:left}#main-menu>div.icon{height:45px;width:45px}#main-menu>div.icon>img{margin:5px 0 0 6px}#main-menu>div.icon.selected{background:#000}#main-menu>div.icon>a{display:block;height:45px;width:45px}#main-menu #mod-plugins{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-45px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-plugins:hover{background-color:#000;background-position:-45px bottom}#main-menu #mod-plugins.selected{background-color:#000;background-position:-45px bottom}#main-menu #mod-cloud-plugins{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-315px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-cloud-plugins:hover{background-color:#000;background-position:-315px bottom}#main-menu #mod-cloud-plugins.selected{background-color:#000;background-position:-315px bottom}#main-menu #mod-pedalboard{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-90px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-pedalboard:hover{background-color:#000;background-position:-90px bottom}#main-menu #mod-pedalboard.selected{background-color:#000;background-position:-90px bottom}#main-menu #mod-bank{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-135px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-bank:hover{background-color:#000;background-position:-135px bottom}#main-menu #mod-bank.selected{background-color:#000;background-position:-135px bottom}#main-menu #mod-file-manager{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-180px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-file-manager:hover{background-color:#000;background-position:-180px bottom}#main-menu #mod-file-manager.selected{background-color:#000;background-position:-180px bottom}#main-menu #banks-saving{margin:21px 0 0 6px;font-size:10px;color:#666;display:none}#main-menu #mod-show-midi-port,#main-menu #mod-transport-icon,#main-menu .mod-bypass,#main-menu #mod-cpu,#main-menu #mod-ram,#main-menu #mod-xruns,#main-menu #mod-buffersize,#main-menu #mod-cpu-stats{background-position:8px 50%;background-repeat:no-repeat;padding-left:40px;padding-right:12px;color:#fff;float:right;font-size:11px;font-weight:bold;line-height:4.5;text-transform:uppercase;width:auto !important;transition:all .33s}#main-menu #mod-show-midi-port:hover,#main-menu #mod-transport-icon:hover,#main-menu .mod-bypass:hover,#main-menu #mod-cpu:hover,#main-menu #mod-ram:hover,#main-menu #mod-xruns:hover,#main-menu #mod-buffersize:hover,#main-menu #mod-cpu-stats:hover{background-color:#000}#main-menu #mod-show-midi-port.selected,#main-menu #mod-transport-icon.selected,#main-menu .mod-bypass.selected,#main-menu #mod-cpu.selected,#main-menu #mod-ram.selected,#main-menu #mod-xruns.selected,#main-menu #mod-buffersize.selected,#main-menu #mod-cpu-stats.selected{background-color:#333}#main-menu #mod-show-midi-port{background-image:url(../img/icons/25/midi.png)}#main-menu #mod-transport-icon{background-image:url(../img/icons/25/stop.png);font-family:monospace;width:10.5em !important;text-align:right}#main-menu #mod-transport-icon.playing{background-image:url(../img/icons/25/transport.png)}#main-menu .mod-bypass{background-image:url(../img/icons/25/bypass.png)}#main-menu .mod-bypass.bypassed{background-color:#883996}#main-menu #mod-cpu,#main-menu #mod-ram{cursor:default;width:115px !important;padding:0 !important;background:linear-gradient(to right, #1e5799 0, #2989d8 69%, #ff6430 78%, #ff6430 93%, #c40000 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#1e5799', endColorstr='#c40000', GradientType=1)}#main-menu #mod-cpu .progress-title{background-image:url(../img/icons/25/cpu.png);background-position:0 50%;background-repeat:no-repeat;padding-left:30px}#main-menu #mod-ram .progress-title{background-image:url(../img/icons/25/ram.png);background-position:0 50%;background-repeat:no-repeat;padding-left:30px}#main-menu #mod-xruns{background-image:url(../img/icons/25/cpu.png);width:115px !important}#main-menu #mod-cpu-stats{background-image:url(../img/icons/25/cpu.png);width:155px !important;text-transform:none}#main-menu #mod-buffersize{background-image:url(../img/icons/25/cpu.png);width:125px !important}#main-menu .progress{height:45px;margin-bottom:0;overflow:hidden;float:right;background-color:#111;-webkit-transition:width 1s;transition:width 1s}#main-menu .progress .bar{float:right;background:#222 !important}#main-menu .progress-title{position:absolute;top:0;height:45px;margin:0;margin-left:1em;color:white}#main-menu .status{background-color:#111;background-position:3px 50%;background-size:36px;background-repeat:no-repeat;border-color:#2b2b2b;border-style:solid;border-width:0 0 0 1px;float:right}#main-menu #mod-settings{font-family:fontello !important;font-size:28px;padding-top:3px;padding-left:10px;position:relative;color:#fff;opacity:.66;cursor:pointer;transition:all .33s}#main-menu #mod-settings:after{content:'\e802'}#main-menu #mod-settings:hover{opacity:1}#main-menu #mod-update{background-image:url(../img/icons/36/update.png)}#main-menu #mod-update.uptodate{background-image:url(../img/icons/36/ok.png)}#main-menu #mod-status{background-image:url(../img/icons/36/disconnect.png)}#main-menu #mod-status.online{background-image:url(../img/icons/36/disconnect.png)}#main-menu #mod-devices{background-image:url(../img/icons/36/control-chain.png)}#cloud_install_all{background-image:url(../img/icons/36/save.png);float:right;font-size:21px}#cloud_update_all{background-image:url(../img/icons/36/update.png);float:right;font-size:21px}header#pedalboard-actions,#bank-library header,#cloud-plugins-library header,#pedalboards-library header,#file-manager-library header{display:block;left:0;height:45px;line-height:45px;position:fixed;right:0;top:0;box-shadow:0 1px 10px rgba(0,0,0,0.1);z-index:5;background-color:#111;background-position:3px 50%;background-size:36px;background-repeat:no-repeat;background-image:url(../img/icons/36/mod.png);padding-left:45px}header#pedalboard-actions .help,#bank-library header .help,#cloud-plugins-library header .help,#pedalboards-library header .help,#file-manager-library header .help{background-image:url(../img/icn-help-16x16.png);background-position:center center;background-repeat:no-repeat;display:inline-block;height:21px;width:21px}header#pedalboard-actions .close,#bank-library header .close,#cloud-plugins-library header .close,#pedalboards-library header .close,#file-manager-library header .close{opacity:1;position:absolute;right:10px;top:16px}header#pedalboard-actions .drop-menu,#bank-library header .drop-menu,#cloud-plugins-library header .drop-menu,#pedalboards-library header .drop-menu,#file-manager-library header .drop-menu{position:absolute;display:none;list-style-type:none;padding:0;background-color:rgba(0,0,0,0.8);color:#999;width:150px}header#pedalboard-actions .drop-menu:hover,#bank-library header .drop-menu:hover,#cloud-plugins-library header .drop-menu:hover,#pedalboards-library header .drop-menu:hover,#file-manager-library header .drop-menu:hover{display:block}header#pedalboard-actions .drop-menu li,#bank-library header .drop-menu li,#cloud-plugins-library header .drop-menu li,#pedalboards-library header .drop-menu li,#file-manager-library header .drop-menu li{padding:5px 20px}header#pedalboard-actions .drop-menu li:hover,#bank-library header .drop-menu li:hover,#cloud-plugins-library header .drop-menu li:hover,#pedalboards-library header .drop-menu li:hover,#file-manager-library header .drop-menu li:hover{color:white;background-color:#883996}header#pedalboard-actions .drop-menu .sub-menu,#bank-library header .drop-menu .sub-menu,#cloud-plugins-library header .drop-menu .sub-menu,#pedalboards-library header .drop-menu .sub-menu,#file-manager-library header .drop-menu .sub-menu{display:none;list-style-type:none;padding:0;position:absolute;left:150px;height:14em;overflow-y:auto;overflow-x:hidden;margin-top:-50px;background-color:rgba(0,0,0,0.8);width:15em;border-left:3px solid #666}header#pedalboard-actions .drop-menu .sub-menu:hover,#bank-library header .drop-menu .sub-menu:hover,#cloud-plugins-library header .drop-menu .sub-menu:hover,#pedalboards-library header .drop-menu .sub-menu:hover,#file-manager-library header .drop-menu .sub-menu:hover{display:block}header#pedalboard-actions .menu-trigger:hover+.drop-menu,#bank-library header .menu-trigger:hover+.drop-menu,#cloud-plugins-library header .menu-trigger:hover+.drop-menu,#pedalboards-library header .menu-trigger:hover+.drop-menu,#file-manager-library header .menu-trigger:hover+.drop-menu{display:block}header#pedalboard-actions .sub-menu-trigger:hover ul,#bank-library header .sub-menu-trigger:hover ul,#cloud-plugins-library header .sub-menu-trigger:hover ul,#pedalboards-library header .sub-menu-trigger:hover ul,#file-manager-library header .sub-menu-trigger:hover ul{display:block}header#pedalboard-actions button,#bank-library header button,#cloud-plugins-library header button,#pedalboards-library header button,#file-manager-library header button{background-position:8px 50%;background-size:25px;background-repeat:no-repeat;padding:0 12px 0 40px;background-color:transparent;border:none;height:45px;line-height:45px;opacity:.66;color:white;transition:all .33s}header#pedalboard-actions button:hover,#bank-library header button:hover,#cloud-plugins-library header button:hover,#pedalboards-library header button:hover,#file-manager-library header button:hover,header#pedalboard-actions button.selected,#bank-library header button.selected,#cloud-plugins-library header button.selected,#pedalboards-library header button.selected,#file-manager-library header button.selected{opacity:1;background-color:black}header#pedalboard-actions button.js-save,#bank-library header button.js-save,#cloud-plugins-library header button.js-save,#pedalboards-library header button.js-save,#file-manager-library header button.js-save{background-image:url(../img/icons/25/save.png)}header#pedalboard-actions button.js-save-as,#bank-library header button.js-save-as,#cloud-plugins-library header button.js-save-as,#pedalboards-library header button.js-save-as,#file-manager-library header button.js-save-as{background-image:url(../img/icons/25/save.png)}header#pedalboard-actions button.js-preset,#bank-library header button.js-preset,#cloud-plugins-library header button.js-preset,#pedalboards-library header button.js-preset,#file-manager-library header button.js-preset{background-image:url(../img/icons/36/presets.png);background-size:30px 30px}header#pedalboard-actions button#js-preset-enabler:hover,#bank-library header button#js-preset-enabler:hover,#cloud-plugins-library header button#js-preset-enabler:hover,#pedalboards-library header button#js-preset-enabler:hover,#file-manager-library header button#js-preset-enabler:hover,header#pedalboard-actions button#js-preset-enabler.selected,#bank-library header button#js-preset-enabler.selected,#cloud-plugins-library header button#js-preset-enabler.selected,#pedalboards-library header button#js-preset-enabler.selected,#file-manager-library header button#js-preset-enabler.selected{background-color:#883996}header#pedalboard-actions button.js-reset,#bank-library header button.js-reset,#cloud-plugins-library header button.js-reset,#pedalboards-library header button.js-reset,#file-manager-library header button.js-reset{background-image:url(../img/icons/25/new.png)}header#pedalboard-actions button.js-reset:hover,#bank-library header button.js-reset:hover,#cloud-plugins-library header button.js-reset:hover,#pedalboards-library header button.js-reset:hover,#file-manager-library header button.js-reset:hover,header#pedalboard-actions button.js-reset.selected,#bank-library header button.js-reset.selected,#cloud-plugins-library header button.js-reset.selected,#pedalboards-library header button.js-reset.selected,#file-manager-library header button.js-reset.selected{background-color:#883996}header#pedalboard-actions button.js-cv-addressing,#bank-library header button.js-cv-addressing,#cloud-plugins-library header button.js-cv-addressing,#pedalboards-library header button.js-cv-addressing,#file-manager-library header button.js-cv-addressing{background-image:url(../img/icons/25/cv.png)}header#pedalboard-actions button.js-cv-addressing:hover,#bank-library header button.js-cv-addressing:hover,#cloud-plugins-library header button.js-cv-addressing:hover,#pedalboards-library header button.js-cv-addressing:hover,#file-manager-library header button.js-cv-addressing:hover,header#pedalboard-actions button.js-cv-addressing.selected,#bank-library header button.js-cv-addressing.selected,#cloud-plugins-library header button.js-cv-addressing.selected,#pedalboards-library header button.js-cv-addressing.selected,#file-manager-library header button.js-cv-addressing.selected{background-color:#883996}header#pedalboard-actions button.js-cloud,#bank-library header button.js-cloud,#cloud-plugins-library header button.js-cloud,#pedalboards-library header button.js-cloud,#file-manager-library header button.js-cloud{background-image:url(../img/icn-share.png);background-size:32px;background-position:5px center}header#pedalboard-actions h1,#bank-library header h1,#cloud-plugins-library header h1,#pedalboards-library header h1,#file-manager-library header h1{font-weight:2em;text-transform:uppercase;font-weight:normal;color:#999;display:inline-block;font-size:24px;line-height:49px;height:45px;padding:0}header#pedalboard-actions{box-shadow:0 2px 12px black}.tooltip{background:white;bottom:50px;height:auto;padding:7px;position:absolute}.tooltip .text{color:#222;font-weight:bold;line-height:1;text-align:center}.tooltip .arrow{border-bottom:9px solid transparent;border-left:9px solid transparent;border-right:9px solid transparent;border-top:9px solid white;height:0;position:absolute;bottom:-18px;right:14px;width:0}#mod-devices-window,#mod-upgrade{background:white;color:black;font-size:15px;max-height:350px;overflow:auto;position:absolute;right:10px;bottom:50px;z-index:100}#mod-devices-window .progressbar-wrapper,#mod-upgrade .progressbar-wrapper{border:1px solid #444;height:20px;width:330px}#mod-devices-window .progressbar,#mod-upgrade .progressbar{background-color:#88f;height:18px}#mod-devices-window{max-width:600px;min-width:500px}#mod-upgrade{max-width:450px;min-width:350px}#mod-transport-window{bottom:48px;right:310px;top:initial;left:initial;background:rgba(17,17,17,0.9);padding:50px 40px 15px 40px;z-index:999}#mod-transport-window .mod-midi-icon{width:auto !important;padding-left:25px;background-position:5% 45% !important;background-image:url(../img/icons/25/midi.png);background-size:15px;background-repeat:no-repeat}#plugins-library{bottom:45px;height:166px;left:0;position:absolute;right:0;transition:all .33s;transition:opacity .5s;transition:height .5s}#plugins-library.folded{height:38px}#plugins-library.folded #plugins-library-settings-window{bottom:0}#plugins-library.fade{opacity:.2}#plugins-library .fold{cursor:pointer;display:block !important;float:right;height:36px;position:absolute;top:0;right:0;width:36px;background-color:rgba(17,17,17,0.66);background-image:url(../img/icons/25/pinned.png);background-position:center center;background-repeat:no-repeat}#plugins-library .folded{cursor:pointer;display:block;float:right;height:36px;position:absolute;top:0;right:0;width:36px}#plugins-library.auto .fold{background-image:url(../img/icons/25/autohide.png)}#plugins-library .settings{background-color:rgba(17,17,17,0.66);background-image:url(../img/icn-search-white.png);background-position:center center;background-repeat:no-repeat;cursor:pointer;display:block;height:36px;position:absolute;right:37px;top:0;width:36px}#plugins-library #plugins-library-settings-window{background-color:rgba(17,17,17,0.66);bottom:130px;color:#fff;position:absolute;right:74px;transition:bottom .5s}#plugins-library #plugins-library-settings-window h1{padding:0}#plugins-library #plugins-library-settings-window form{padding:4px;border:0;margin:0}#plugins-library #plugins-library-settings-window label{margin-right:10px}#plugins-library #plugins-library-settings-window label.checkbox:last-child{margin-right:0}#plugins-library #plugins-library-settings-window .control-group{position:relative}#plugins-library #plugins-library-settings-window input{width:120px;height:28px;line-height:28px;padding:0 6px;outline:0;border:0}#plugins-library #plugins-library-settings-window .input-clean{position:absolute;top:50%;right:8px;bottom:auto;left:auto;transform:translateY(-50%);margin:0 !important;border-radius:100%}#plugins-library ul{list-style:none;margin:6px 73px 0 0;overflow:hidden;padding:10px 0 0 0;position:relative;width:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}#plugins-library ul li{background:rgba(17,17,17,0.66);color:#bbb;cursor:pointer !important;display:inline-block;float:left;font-size:11px;margin:0 1px 0 0;padding:3px 11px;text-shadow:1px 1px 0 #000;text-transform:uppercase;border-top:solid 3px transparent;transition-property:border-radius,color,margin-top,padding,border-top;transition-duration:.33s}#plugins-library ul li:hover{margin-top:-6px;padding:6px 11px;color:white}#plugins-library ul li.selected{color:white;font-weight:bold;margin-top:-6px;padding:6px 11px;border-top:solid 3px #883996}#plugins-library.scrolling .plugins-wrapper{transition:all .1s !important}#plugins-library #plugins-list{background:rgba(17,17,17,0.66);height:140px;overflow:hidden;position:relative;width:100%;z-index:1}#plugins-library #plugins-list .nav{background:url(../img/nav-left.png) 50% 50% no-repeat;cursor:pointer;position:absolute;top:0;width:50px;height:100%;opacity:.66}#plugins-library #plugins-list .nav:hover{opacity:1}#plugins-library #plugins-list .nav-left{left:0}#plugins-library #plugins-list .nav-right{background:url(../img/nav-right.png) 50% 50% no-repeat;right:0}#plugins-library #plugins-list #plugins-results{bottom:0;left:50px;position:absolute;overflow:hidden;right:50px;top:0}#plugins-library #plugins-list .plugins-wrapper{position:absolute;display:inline-block;white-space:nowrap;transition:all .5s;top:120px;left:0;opacity:0}#plugins-library #plugins-list .plugins-wrapper.selected{top:0;opacity:1}#plugins-library #plugins-list .plugins-wrapper .plugin{color:#fff;font-size:11px;font-weight:bold;height:108px;padding:14px 12px 10px;position:relative;text-align:center;display:inline-block}#plugins-library #plugins-list .plugins-wrapper .plugin .thumb{height:67px;cursor:pointer !important;position:relative;display:flex;flex-direction:column;justify-content:center}#plugins-library #plugins-list .plugins-wrapper .plugin .thumb:active{cursor:-webkit-grabbing !important;cursor:grabbing !important}#plugins-library #plugins-list .plugins-wrapper .plugin .thumb img{height:auto;margin:0 auto 3px;width:auto;pointer-events:none;max-width:256px;max-height:64px}#plugins-library #plugins-list .plugins-wrapper .plugin .status{display:block;height:16px;margin:0 auto;width:16px}#plugins-library #plugins-list .plugins-wrapper .plugin .status.demo{display:inline-block;width:auto;height:auto;margin:0}#plugins-library #plugins-list .plugins-wrapper .plugin .status.installed{background-image:url(../img/icn-installed.png);background-position:center center;background-repeat:no-repeat}#plugins-library #plugins-list .plugins-wrapper .plugin .status.outdated{background-image:url(../img/icn-outdated.png);background-position:center center;background-repeat:no-repeat}#plugins-library #plugins-list .plugins-wrapper .plugin .status.blocked{background-image:url(../img/icn-blocked.png);background-position:center center;background-repeat:no-repeat}#plugins-library #plugins-list .plugins-wrapper .plugin .author{display:block;line-height:1.5}#plugins-library #plugins-list .plugins-wrapper .plugin .title{display:block;line-height:1.25}#plugins-library #plugins-list .plugins-wrapper .plugin .rating{background-image:url(../img/icn-rating.png);background-position:center top;background-repeat:no-repeat;display:block;height:16px;width:132px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.one{background-position:center -15px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.two{background-position:center -30px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.three{background-position:center -45px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.four{background-position:center -60px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.five{background-position:center -75px}#plugins-library #plugins-list .plugins-wrapper .plugin .downloads{display:block}#plugins-library #plugins-list .plugins-wrapper .plugin .demo-container{position:absolute;left:0;bottom:3px;right:0;text-align:center}.plugin-info{background-color:rgba(20,20,20,0.9);bottom:0;color:#ccc;left:0;overflow-y:scroll;position:fixed;right:0;top:0;z-index:40}.plugin-info .box{margin-top:3em;margin-bottom:3em;background-color:#111;padding:15px;border:2px solid #333}.plugin-info .box .row-fluid{position:relative}.plugin-info .close{color:#fff;font-size:12px;font-weight:bold;opacity:1;position:absolute;right:10px;text-shadow:none;text-transform:uppercase;top:10px}.plugin-info .screenshot{padding:18px;min-width:3em;min-height:9em;display:inline-block;z-index:0}.plugin-info .screenshot div.beta{background-color:#ff2927;font-family:"cooper hewitt",sans-serif;font-weight:700;padding:5px 10px;color:white;position:absolute;top:18px;left:30px;font-size:16px}.plugin-info figure{position:relative;min-height:1px;padding-right:15px;padding-left:15px}@media (min-width:992px){.plugin-info figure{float:left;width:33.33333333%}}.plugin-info figure:hover{z-index:10 !important}.plugin-info .plugin-description{position:relative;min-height:1px;padding-right:15px;padding-left:15px;height:100%;display:block;position:a;padding-left:45px;background:-moz-linear-gradient(left, rgba(17,17,17,0.1) 1em, #111 2em);background:-webkit-gradient(linear, left top, right top, color-stop(1em, rgba(17,17,17,0.1)), color-stop(2em, #111));background:-webkit-linear-gradient(left, rgba(17,17,17,0.1) 1em, #111 2em);background:-o-linear-gradient(left, rgba(17,17,17,0.1) 1em, #111 2em);background:-ms-linear-gradient(left, rgba(17,17,17,0.1) 1em, #111 2em);background:linear-gradient(to right, rgba(17,17,17,0.1) 1em, #111 2em);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffffff', endColorstr='#ffffff', GradientType=1)}@media (min-width:992px){.plugin-info .plugin-description{float:left;width:58.33333333%}}.plugin-info .plugin-description>p{font-size:16px;margin-bottom:27px;white-space:pre-wrap}.plugin-info header{padding:18px 0 0}.plugin-info header h1{display:inline-block;vertical-align:middle;color:#fff;font-size:27px;font-weight:bold;line-height:1;margin:0;padding:0}.plugin-info header h1 span{font-size:16px;line-height:1;margin:0;padding:0}.plugin-info .version{color:#ccc;font-size:12px;font-weight:bold;text-transform:uppercase}.plugin-info .version span.bold{color:#fff;font-size:14px}.plugin-info .version button{font-weight:bold;margin-top:6px;text-transform:uppercase}.plugin-info .rating{background:url(../img/icn-rating.png) no-repeat -12px top;display:inline-block;height:14px;width:84px}.plugin-info .rating.one{background:url(../img/icn-rating.png) no-repeat -12px -15px}.plugin-info .rating.two{background:url(../img/icn-rating.png) no-repeat -12px -30px}.plugin-info .rating.three{background:url(../img/icn-rating.png) no-repeat -12px -45px}.plugin-info .rating.four{background:url(../img/icn-rating.png) no-repeat -12px -60px}.plugin-info .rating.five{background:url(../img/icn-rating.png) no-repeat -12px -75px}.plugin-info .rating-label{color:#fff;font-size:12px;font-weight:bold;text-transform:uppercase}.plugin-info .my-rate{width:15px;height:15px;margin:0;padding:0;float:left;cursor:pointer}.plugin-info .plugin-specs{text-align:center;width:100%;border-color:#333}.plugin-info .plugin-specs th{background:#333;color:#fff;border-color:#333;text-align:center}.plugin-info .plugin-specs td{border-color:#333}.plugin-info .plugin-specs td:first-child{width:40%}.plugin-info .plugin-specs td:not(first-child){width:20%}.plugin-info .comments-reviews{border-top:1px solid #333;margin-top:10px;padding-top:10px}.plugin-info .comments-reviews h1{color:#fff;font-size:21px;line-height:2;margin:0;padding:0}.plugin-info .reviews{border-bottom:1px solid #333;margin:9px 0 18px 0;padding:9px 0 18px 0}.plugin-info article.review{margin:18px 0}.plugin-info article.review h1{color:#fff;font-size:14px}.plugin-info article.review h1 .rating{margin-left:6px}.plugin-info article.review h1 .rating-label{color:#ccc;display:inline;font-size:12px;font-weight:normal;margin-left:6px}.plugin-info article.review .bar{background:#333;display:block;height:12px;margin:2px 6px 0;position:relative;width:100px}.plugin-info article.review .progress-bar{background:#444;bottom:0;display:block;height:10px;left:0;margin:1px;position:absolute;right:0;top:0;width:50%}.plugin-info article.review .rating-number{display:block;font-size:12px;margin-top:-1px}.plugin-info article.comment{border-top:1px solid #333;margin:18px 0 0;padding:18px 0 0}.plugin-info article.comment h1{color:#fff;font-size:16px;line-height:1}.plugin-info article.comment h1 .date{color:#ccc;font-size:12px;font-weight:normal;margin-left:6px}.plugin-info .comments{margin:18px 0;padding:18px 0}.plugin-info .comments button{margin-top:18px}.plugin-info form.comment-form{border-top:1px solid #333;margin:18px 0;padding:18px 0}.plugin-info form.comment-form label{font-size:12px;font-weight:bold;text-transform:uppercase}.plugin-info form.comment-form textarea{height:180px}.plugin-info .favorite-button{display:inline-block;vertical-align:middle;width:30px;height:30px;background:url(../img/icons/25/star-border.png) no-repeat center center;cursor:pointer;opacity:.5}.plugin-info .favorite-button:hover{opacity:.8}.plugin-info .favorite-button.favorite{opacity:1;background:url(../img/icons/25/star.png) no-repeat center center}.plugin-info .favorite-button.favorite:hover{opacity:1}.plugin-info .online-button-href{display:inline-block;margin-bottom:10px}.plugin-info .status.demo{display:inline-block;padding:4px 8px;margin:2px 0;font-size:.8em}.clearfix:after,.container:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after{content:"\0020";display:block;height:0;clear:both;visibility:hidden;overflow:hidden}.clearfix,.container,.dl-horizontal dd,.container,.container-fluid,.row,.form-horizontal .form-group,.btn-toolbar,.btn-group-vertical>.btn-group,.nav,.navbar,.navbar-header,.navbar-collapse,.pager,.panel-body{display:block}.clear{clear:both}.blend{opacity:.25}.box{padding:10px}.alignleft{float:left}.alignright{float:right}.textcenter{text-align:center}.textright{text-align:right}.bottom{margin-bottom:0 !important;padding-bottom:0 !important}.left{margin-left:0 !important;padding-left:0 !important}.right{margin-right:0 !important;padding-right:0 !important}.top{margin-top:0 !important;padding-top:0 !important}.nobullets{list-style:none}.mod-hidden{display:none}.mod-init-hidden{opacity:0}::selection{background:transparent}.mod-box{background:white;left:50%;margin:-150px -168px;padding:18px;position:absolute;top:50%;width:300px}.mod-box:not(#midi-ports-box) label{display:inline-block;float:left;line-height:2;width:96px}.mod-box .mod-controls{float:left}.mod-box input[type="text"]{width:190px}.mod-box input[type="password"]{width:190px}.blocker{background:rgba(17,17,17,0.9);color:white;display:block;position:fixed;width:100% !important;height:100% !important;top:0;left:0;text-align:center;z-index:99999}.blocker p{position:absolute;top:50%;width:100%;margin-top:56px;font-size:24px}.blocker.screen-disconnected{background:rgba(17,17,17,0.9) url(../img/icons/blocked.svg) 50% 50% no-repeat;background-size:100px 100px}.blocker.screen-disconnected .button{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);margin-top:120px}.plugin-wait{background:url(../img/loading-effect.gif) no-repeat center center;border:1px solid #ccc}.button{background:#883996;color:white;display:inline-block;cursor:pointer;line-height:25px;padding:5px 15px;transition:all .33s;border:none}.button:hover{background-color:#f29446;box-shadow:0 1px 5px black}.button.icon{padding-left:40px;background-position:10px 50%;background-repeat:no-repeat;background-size:25px}.drag-handle{bottom:0;cursor:-webkit-grabbing !important;cursor:grabbing !important;left:0;position:absolute;right:0;top:0;z-index:20}.demo-plugin{background:url("/img/trial.svg") no-repeat center center;background-size:contain;opacity:.4;z-index:11 !important}.demo-mask{width:100%;height:100%;left:0;right:0;top:0;bottom:0;position:absolute}.demo-plugin-light{opacity:.1 !important}.actions{z-index:20}@keyframes flashlight{0%{box-shadow:0 0 0 transparent}50%{box-shadow:0 0 10px white}100%{box-shadow:0 0 0 transparent}}.alert{background:rgba(17,17,17,0.9);border-width:0 0 0 6px;border-color:#883996;color:white;box-shadow:0 3px 10px black;margin-top:5px !important}.alert .js-close{color:white}.alert-warning{border-color:#f29446}.alert-danger{border-color:#c00}#go-back{width:200px;text-align:left;margin:20px 0 0 0;background:none;border:none;font-size:15px;padding:5px;color:#fff !important}#go-back span{font-family:fontello !important}#go-back span:before{content:'\e804'}#go-back:hover{text-decoration:none}.unstable-warning{float:left;background-color:#ffebb1;padding:5px;border-radius:10px} \ No newline at end of file +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,*:before,*:after{color:#000 !important;text-shadow:none !important;background:transparent !important;box-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#883996;text-decoration:none}a:hover,a:focus{color:#56245f;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:0}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:0;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#999}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{padding:.2em;background-color:#f29446}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#999}.text-primary{color:#883996}a.text-primary:hover,a.text-primary:focus{color:#662b71}.text-success{color:#3c763d}a.text-success:hover,a.text-success:focus{color:#2b542c}.text-info{color:#3a633d}a.text-info:hover,a.text-info:focus{color:#274329}.text-warning{color:#fff}a.text-warning:hover,a.text-warning:focus{color:#e6e6e6}.text-danger{color:#a94442}a.text-danger:hover,a.text-danger:focus{color:#843534}.bg-primary{color:#fff;background-color:#883996}a.bg-primary:hover,a.bg-primary:focus{background-color:#662b71}.bg-success{background-color:#dff0d8}a.bg-success:hover,a.bg-success:focus{background-color:#c1e2b3}.bg-info{background-color:#a6ecb1}a.bg-info:hover,a.bg-info:focus{background-color:#7ce38c}.bg-warning{background-color:#f29446}a.bg-warning:hover,a.bg-warning:focus{background-color:#ef7816}.bg-danger{background-color:#f2dede}a.bg-danger:hover,a.bg-danger:focus{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#999}blockquote footer:before,blockquote small:before,blockquote .small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:""}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:"\00A0 \2014"}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:0}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:0;box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:0}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:100%}}@media (min-width:992px){.container{width:970px}}@media (min-width:1240px){.container{width:1270px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*="col-"]{padding-right:0;padding-left:0}.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1240px){.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}table col[class*="col-"]{position:static;display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#999;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#a6ecb1}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#91e89f}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#f29446}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#f0862e}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none;appearance:none}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);box-shadow:inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{line-height:34px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:30px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:46px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-top:4px \9;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:0}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:0}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm textarea.form-control,.form-group-sm select[multiple].form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:0}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:0}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg textarea.form-control,.form-group-lg select[multiple].form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.33}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback,.input-group-lg+.form-control-feedback,.form-group-lg .form-control+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback,.input-group-sm+.form-control-feedback,.form-group-sm .form-control+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#fff}.has-warning .form-control{border-color:#fff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-warning .input-group-addon{color:#fff;background-color:#f29446;border-color:#fff}.has-warning .form-control-feedback{color:#fff}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;white-space:nowrap;vertical-align:middle;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);opacity:.65;-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:focus,.btn-default.focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#883996;border-color:#773284}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#662b71;border-color:#230f27}.btn-primary:hover{color:#fff;background-color:#662b71;border-color:#4f2157}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#662b71;background-image:none;border-color:#4f2157}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#4f2157;border-color:#230f27}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#883996;border-color:#773284}.btn-primary .badge{color:#883996;background-color:#fff}.btn-success{color:#fff;background-color:#883996;border-color:#773284}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#662b71;border-color:#230f27}.btn-success:hover{color:#fff;background-color:#662b71;border-color:#4f2157}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#662b71;background-image:none;border-color:#4f2157}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#fff;background-color:#4f2157;border-color:#230f27}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#883996;border-color:#773284}.btn-success .badge{color:#883996;background-color:#fff}.btn-info{color:#fff;background-color:#883996;border-color:#773284}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#662b71;border-color:#230f27}.btn-info:hover{color:#fff;background-color:#662b71;border-color:#4f2157}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#662b71;background-image:none;border-color:#4f2157}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#fff;background-color:#4f2157;border-color:#230f27}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#883996;border-color:#773284}.btn-info .badge{color:#883996;background-color:#fff}.btn-warning{color:#fff;background-color:#f29446;border-color:#f0862e}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#ef7816;border-color:#95490a}.btn-warning:hover{color:#fff;background-color:#ef7816;border-color:#d3680f}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ef7816;background-image:none;border-color:#d3680f}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#fff;background-color:#d3680f;border-color:#95490a}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#f29446;border-color:#f0862e}.btn-warning .badge{color:#f29446;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;background-image:none;border-color:#ac2925}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#883996;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#56245f;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:0}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:0}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:0}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height, visibility;transition-property:height, visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid \9;border-right:4px solid transparent;border-left:4px solid transparent}.dropup,.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:0;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#883996;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#999;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid \9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle="buttons"]>.btn input[type="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:0}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:0}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:0}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:0}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:0}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#883996}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:0 0 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:0 0 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:0}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#883996}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:0 0 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:0}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:0}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-right:-15px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left !important}.navbar-right{float:right !important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#999}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:0}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:0}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#883996;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{z-index:2;color:#56245f;background-color:#eee;border-color:#ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:3;color:#fff;cursor:default;background-color:#883996;border-color:#883996}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.33}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:0;border-bottom-left-radius:0}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:0;border-bottom-left-radius:0}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#883996}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#662b71}.label-success{background-color:#883996}.label-success[href]:hover,.label-success[href]:focus{background-color:#662b71}.label-info{background-color:#883996}.label-info[href]:hover,.label-info[href]:focus{background-color:#662b71}.label-warning{background-color:#f29446}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ef7816}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#883996;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:0}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:0;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#883996}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:0}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#3a633d;background-color:#a6ecb1;border-color:#89e689}.alert-info hr{border-top-color:#75e174}.alert-info .alert-link{color:#274329}.alert-warning{color:#fff;background-color:#f29446;border-color:#f0662e}.alert-warning hr{border-top-color:#ef5416}.alert-warning .alert-link{color:#e6e6e6}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:0;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#883996;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#883996}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#883996}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#f29446}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{color:#999;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#999}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#883996;border-color:#883996}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#ddb7e4}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#3a633d;background-color:#a6ecb1}a.list-group-item-info,button.list-group-item-info{color:#3a633d}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#3a633d;background-color:#91e89f}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#3a633d;border-color:#3a633d}.list-group-item-warning{color:#fff;background-color:#f29446}a.list-group-item-warning,button.list-group-item-warning{color:#fff}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#fff;background-color:#f0862e}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#fff;border-color:#fff}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:-1px;border-top-right-radius:-1px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a,.panel-title>small,.panel-title>.small,.panel-title>small>a,.panel-title>.small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:-1px;border-top-right-radius:-1px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-right:15px;padding-left:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-left-radius:-1px;border-top-right-radius:-1px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:-1px;border-top-right-radius:-1px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:-1px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:-1px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:-1px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:-1px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:0}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#444}.panel-default>.panel-heading{color:#999;background-color:#444;border-color:#444}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#444}.panel-default>.panel-heading .badge{color:#444;background-color:#999}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#444}.panel-primary{border-color:#883996}.panel-primary>.panel-heading{color:#fff;background-color:#883996;border-color:#883996}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#883996}.panel-primary>.panel-heading .badge{color:#883996;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#883996}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#89e689}.panel-info>.panel-heading{color:#3a633d;background-color:#a6ecb1;border-color:#89e689}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#89e689}.panel-info>.panel-heading .badge{color:#a6ecb1;background-color:#3a633d}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#89e689}.panel-warning{border-color:#f0662e}.panel-warning>.panel-heading{color:#fff;background-color:#f29446;border-color:#f0662e}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#f0662e}.panel-warning>.panel-heading .badge{color:#f29446;background-color:#fff}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#f0662e}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:0}.well-sm{padding:9px;border-radius:0}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none;appearance:none}.tooltip{position:absolute;z-index:1030;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:12px;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:0}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.clearfix,.dl-horizontal dd,.container,.container-fluid,.row,.form-horizontal .form-group,.btn-toolbar,.btn-group-vertical>.btn-group,.nav,.navbar,.navbar-header,.navbar-collapse,.pager,.panel-body{display:block}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after{display:table;content:" "}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (max-width:767px){.visible-xs-block{display:block !important}}@media (max-width:767px){.visible-xs-inline{display:inline !important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table !important}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block !important}}@media (min-width:992px) and (max-width:1239px){.visible-md{display:block !important}table.visible-md{display:table !important}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:992px) and (max-width:1239px){.visible-md-block{display:block !important}}@media (min-width:992px) and (max-width:1239px){.visible-md-inline{display:inline !important}}@media (min-width:992px) and (max-width:1239px){.visible-md-inline-block{display:inline-block !important}}@media (min-width:1240px){.visible-lg{display:block !important}table.visible-lg{display:table !important}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (min-width:1240px){.visible-lg-block{display:block !important}}@media (min-width:1240px){.visible-lg-inline{display:inline !important}}@media (min-width:1240px){.visible-lg-inline-block{display:inline-block !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1239px){.hidden-md{display:none !important}}@media (min-width:1240px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table !important}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}}#cloud-plugins-library{background:#2c2c2c;bottom:45px;left:0;overflow:auto;position:fixed;right:0;top:0;z-index:2}#cloud-plugins-library h1{color:#fff;display:inline-block;font-size:24px;line-height:35px;margin:0 0 18px 0;padding:0}#cloud-plugins-library header .help{background-image:url(../img/icn-help-16x16.png);background-position:center center;background-repeat:no-repeat;display:inline-block;height:21px;width:21px}#cloud-plugins-library header .close{opacity:1;position:absolute;right:10px;top:16px}#cloud-plugins-library .filter{bottom:45px;left:0;position:fixed;top:45px;right:0;width:240px;overflow:auto}#cloud-plugins-library .filter h1{font-weight:2em;text-transform:uppercase;font-weight:bold;margin:0 0 0 10px}#cloud-plugins-library .filter h2{color:white;font-size:15px;text-transform:uppercase;font-weight:bold;margin-top:1em;margin-left:10px}#cloud-plugins-library .filter .control-group .switch{margin:5px 0 5px 15px;position:relative;width:18rem;height:2.5rem;font-size:0}#cloud-plugins-library .filter .control-group .switch input{position:absolute;top:0;z-index:2;opacity:0;cursor:pointer;height:2.5rem;width:5rem;left:5.5rem;margin:0}#cloud-plugins-library .filter .control-group .switch input:checked{z-index:1}#cloud-plugins-library .filter .control-group .switch input:checked+label{opacity:1;cursor:default}#cloud-plugins-library .filter .control-group .switch input:checked~.toggle-outside .toggle-inside{left:.25rem;background:#883996}#cloud-plugins-library .filter .control-group .switch input:not(:checked)+label:hover{opacity:.5}#cloud-plugins-library .filter .control-group .switch input~input:checked~.toggle-outside .toggle-inside{left:2.65rem;background:#4a4a4a}#cloud-plugins-library .filter .control-group .switch label{color:#fff;opacity:.33;transition:opacity .25s ease;cursor:pointer;font-size:14px;line-height:3rem;display:inline-block;width:5rem;height:100%;margin:0;text-align:center}#cloud-plugins-library .filter .control-group .switch label:first-of-type{text-align:left}#cloud-plugins-library .filter .control-group .switch label:last-of-type{margin-left:5.5rem}#cloud-plugins-library .filter .control-group .switch .toggle-outside{height:100%;border-radius:2rem;padding:.25rem;overflow:hidden;transition:.25s ease all;background:#fff;opacity:.8;position:absolute;width:5rem;left:5.5rem}#cloud-plugins-library .filter .control-group .switch .toggle-inside{border-radius:5rem;background:#4a4a4a;position:absolute;transition:.25s ease all;height:2rem;width:2rem}#cloud-plugins-library .filter .control-group select{width:50px}#cloud-plugins-library .filter .control-group input[type='checkbox']{display:none}#cloud-plugins-library .filter .control-group input[type='checkbox']+label{cursor:pointer;background:#222;color:#fff;display:block;height:32px;line-height:32px;width:100%;margin-bottom:1px;padding-left:15px;border:0 solid #f29446}#cloud-plugins-library .filter .control-group input[type='checkbox']+label:hover{background:#000}#cloud-plugins-library .filter .control-group input[type='checkbox']:checked+label{background:#f29446}#cloud-plugins-library .filter .control-group input[type='checkbox']:checked+label:hover{background:#600e6f}#cloud-plugins-library .filter>.form-group .input-clean{margin-right:8px !important}#cloud-plugins-library .filter .categories{width:100%;margin:0 0 15px 0}#cloud-plugins-library .filter .categories li{display:block;background:#222;color:#888;cursor:pointer;font-weight:bold;margin:0 0 1px 0;padding:0 15px;position:relative;transition:all .33s;line-height:32px;height:32px}#cloud-plugins-library .filter .categories li span{float:right}#cloud-plugins-library .filter .categories li.selected{background:#883996;color:#fff}#cloud-plugins-library .filter .categories li:hover{background:#000}#cloud-plugins-library .filter .categories li.selected:hover{background:#600e6f}.cloud-plugins{left:240px;position:fixed;right:0;top:45px;bottom:45px;background:#111 url(../img/watermark.png) 100% 100% no-repeat;overflow-y:auto}.cloud-plugins .plugins-wrapper{width:100%}.cloud-plugins .cloud-plugin{color:#fff;float:left;width:100%;position:relative;min-height:1px;padding-right:15px;padding-left:15px;height:120px;border:5px solid #000;padding:0 !important;background:rgba(255,255,255,0.07);transition:all .33s;cursor:pointer}@media (min-width:768px){.cloud-plugins .cloud-plugin{float:left;width:50%}}@media (min-width:992px){.cloud-plugins .cloud-plugin{float:left;width:50%}}@media (min-width:1240px){.cloud-plugins .cloud-plugin{float:left;width:33.33333333%}}.cloud-plugins .cloud-plugin:hover{background-color:black}.cloud-plugins .cloud-plugin:hover .description{background:rgba(0,0,0,0.8)}.cloud-plugins .cloud-plugin:hover .description hr{border-color:#f29446}.cloud-plugins .cloud-plugin:hover figure{top:-5px}.cloud-plugins .cloud-plugin:hover div.demo-plugin{opacity:0}.cloud-plugins .cloud-plugin:hover span.price{position:absolute;top:25px;margin-left:98px;padding:3px 0;transform:rotate(0deg) scale(1.3);width:80px}.cloud-plugins .cloud-plugin:hover span.licensed{position:absolute;top:25px;margin-left:98px;padding:3px 0;transform:rotate(0deg) scale(1.3);width:113px;text-align:center}.cloud-plugins .cloud-plugin:hover span.coming-soon{position:absolute;top:25px;margin-left:98px;padding:3px 0;transform:rotate(0deg) scale(1.3);width:113px;text-align:center}.cloud-plugins .cloud-plugin:hover span.price-container{margin-left:170px;width:200px}.cloud-plugins .cloud-plugin figure{height:100px;position:absolute;top:10px;left:20px;margin:0;transition:all .33s linear;display:flex;flex-direction:column;justify-content:center}.cloud-plugins .cloud-plugin figure img{height:auto;margin:0 auto 3px;width:auto;pointer-events:none;max-width:256px;max-height:64px}.cloud-plugins .cloud-plugin span.price{transition:all .33s linear}.cloud-plugins .cloud-plugin span.price-container{transition:all .33s linear}.cloud-plugins .cloud-plugin .cloud-plugin-border{border:1px solid #333;width:100%;height:100%}.cloud-plugins .cloud-plugin .description{transition:all .33s;display:block;line-height:1.5;overflow:hidden;position:absolute;top:1px;right:1px;bottom:1px;left:80px;background:rgba(33,33,33,0.66)}.cloud-plugins .cloud-plugin .description .no_description{color:rgba(255,255,255,0.25)}.cloud-plugins .cloud-plugin .description hr{transition:all .33s;border:none;border-bottom:1px solid #883996;margin:0;position:absolute;top:50%;left:0;width:calc(100% - 5px)}.cloud-plugins .cloud-plugin .description .title{display:block;text-transform:uppercase;font-weight:bold;overflow:hidden;padding:8px 10px 1px 10px}.cloud-plugins .cloud-plugin .description .author{display:block;padding:1px 10px}.cloud-plugins .cloud-plugin .description p{position:absolute;left:10px;right:10px;top:75%;transform:translateY(-50%);height:2.4em;line-height:1.25em;overflow:hidden}.cloud-plugins .cloud-plugin .actions{font-size:11px;font-weight:bold;text-transform:uppercase}.cloud-plugins .cloud-plugin .actions>span{cursor:pointer}.cloud-plugins .cloud-plugin .actions>span:hover{text-decoration:underline}.cloud-plugins .cloud-plugin .actions span.duplicate{margin:0 6px}.cloud-plugins h2{margin-left:23px;font-size:20px;color:#ccc}.cloud-plugins h3{color:#ccc}.cloud-plugins .featured-plugins{position:relative;height:360px;width:100%;display:block}.cloud-plugins .featured-plugins.double,.cloud-plugins .featured-plugins.single{height:490px !important}.cloud-plugins .featured-plugins.double .box,.cloud-plugins .featured-plugins.single .box{transition:all 0s !important}.cloud-plugins .featured-plugins.double .carousel,.cloud-plugins .featured-plugins.single .carousel{height:485px !important}.cloud-plugins .featured-plugins.double .carousel .slick-list,.cloud-plugins .featured-plugins.single .carousel .slick-list{height:485px !important}.cloud-plugins .featured-plugins.double .box{margin:30px 80px !important}.cloud-plugins .featured-plugins.single .box{margin:30px 30px !important}.cloud-plugins .featured-plugins .carousel{position:absolute;top:0;bottom:0;right:90px;left:90px;height:380px !important}.cloud-plugins .featured-plugins .carousel .slick-list{height:380px !important}.cloud-plugins .featured-plugins .carousel .featured{cursor:pointer;margin-top:40px}.cloud-plugins .featured-plugins .carousel .featured .box{padding:4px;border:1px solid #737373;height:300px;opacity:.4;margin:0 24px}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box{border:1px solid #444;text-align:center;height:290px}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .price-container{position:absolute;top:0;left:0;width:90px;height:90px;overflow:hidden;color:#fff}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .price{background-color:#883996;border:1px solid #883996;position:absolute;top:9px;left:-30px;padding:3px 0;transform:rotate(-45deg);min-width:108px;text-align:center}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .price:before{content:"€ "}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .licensed{background-color:#883996;border:1px solid #883996;position:absolute;top:16px;left:-30px;padding:3px 27px;font-size:12px;transform:rotate(-45deg)}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .licensed:before{content:"PURCHASED"}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box h2{color:#c3c3c3;text-align:center}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .img-container{height:113px;width:100%}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .img-container img{vertical-align:middle;transition:all 500ms ease;width:auto;height:auto;margin:auto;display:block;max-height:113px;padding:0 20px;max-width:100%}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box p{color:#e0e0e0;padding:5px 5px 0 5px;height:59px}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box button{background-color:transparent;color:#ff3838;border:1px solid #ff3838;text-transform:uppercase;padding:8px 20px}.cloud-plugins .featured-plugins .carousel .featured.slick-center{height:390px}.cloud-plugins .featured-plugins .carousel .featured.slick-center .box{opacity:1;transform:scale(1.2) translate(0, 0);transition:all 500ms ease}.cloud-plugins .featured-plugins .carousel .slick-arrow{width:60px;height:60px;top:200px}.cloud-plugins .featured-plugins .carousel .slick-arrow:before{font-size:60px}.cloud-plugins .featured-plugins .carousel .slick-prev{left:-70px !important}.cloud-plugins .featured-plugins .carousel .slick-next{right:-70px !important}.plugin-container .status-container{position:absolute;top:0;left:0;right:2px;text-align:right;padding-right:2px}.plugin-container .status{padding:1px 4px;text-align:center;text-transform:uppercase;font-weight:bold;font-size:9px;margin-left:1px}.plugin-container .status.installed{border:1px solid #883996;background-color:#883996}.plugin-container .status.installed:after{content:"installed"}.plugin-container .status.outdated{border:1px solid #f29446;background-color:#f29446;color:#212121}.plugin-container .status.outdated:after{content:"new version"}.plugin-container .price-container{position:absolute;top:0;left:0;width:90px;height:90px;overflow:hidden}.plugin-container .price{background-color:#883996;border:1px solid #883996;position:absolute;top:9px;left:-25px;padding:3px 0;transform:rotate(-45deg);min-width:108px;text-align:center}.plugin-container .price:before{content:"€ "}.plugin-container .licensed{background-color:#883996;border:1px solid #883996;position:absolute;top:21px;left:-30px;padding:3px 27px;font-size:14px;transform:rotate(-45deg)}.plugin-container .licensed:before{content:"PURCHASED"}.plugin-container .coming-soon{background-color:#883996;border:1px solid #883996;position:absolute;top:26px;left:-35px;padding:3px 27px;font-size:12px;transform:rotate(-45deg);width:135px}.plugin-container .coming-soon:before{content:"TRIAL ONLY"}.plugin-container .status.buildEnvironment{border:1px solid #ff2927;background-color:#ff2927}.plugin-container .status.buildEnvironment:after{content:"local"}.plugin-container .status.buildEnvironment.labs:after{content:"labs"}.plugin-container .status.buildEnvironment.dev:after{content:"dev"}.plugin-container .status.buildEnvironment.prod{display:none}.plugin-container .status.buildEnvironment.prod:after{content:""}.plugin-container .demo{border:1px solid #ff2927;background-color:#ff2927}.plugin-container .demo:after{content:"demo"}.plugin-container .description-price-container{position:absolute;top:-17px;left:-17px;overflow:hidden;height:200px}.plugin-container .description-price-container .description-price{background-color:#883996;border:1px solid #883996;position:relative;top:34px;left:-57px;padding:3px 0;transform:rotate(-45deg);min-width:250px;text-align:center;font-size:34px;z-index:11}.plugin-container .description-price-container .description-price:before{content:"€ "}.plugin-container .description-price-container .description-licensed{background-color:#883996;border:1px solid #883996;position:relative;top:50px;left:-60px;padding:3px 0;transform:rotate(-45deg);min-width:280px;text-align:center;font-size:34px;z-index:1}.plugin-container .description-price-container .description-licensed:before{content:"PURCHASED"}.plugin-container .description-price-container .description-coming-soon{background-color:#883996;border:1px solid #883996;position:relative;top:50px;left:-70px;padding:3px 0;transform:rotate(-45deg);min-width:280px;text-align:center;font-size:26px;z-index:1}.plugin-container .description-price-container .description-coming-soon:before{content:"TRIAL ONLY"}.buy-button{margin-top:-20px;position:relative}.buy-button iframe{position:absolute;top:-17px;left:0;min-width:115px;z-index:1}.shopify-fake-button{position:absolute;top:3px;left:0;z-index:0;background-color:#c224b0;font-family:Arial;font-size:12px;opacity:.25;min-width:115px;min-height:34px;padding:9px 15px;text-align:center}#bank-library{bottom:45px;left:0;overflow-x:hidden;overflow-y:auto;position:absolute;right:0;top:0;z-index:2}#bank-library .ui-draggable-dragging{cursor:webkit-dragging !important}#bank-library .mod-banks-drag-item{background:#f29446;color:white;font-weight:bold;z-index:10000;box-shadow:0 5px 15px black;opacity:.5}#bank-library .mod-banks-drag-item .img{display:block;height:auto;margin:0 auto;text-align:center;width:100%;background:url(../img/background.jpg);background-size:auto 100%;background-position:center center;box-shadow:inset 0 3px 6px black;border:1px solid #000}#bank-library .mod-banks-drag-item .title{display:block;line-height:32px;font-weight:bold;overflow:hidden;position:relative;padding-left:5px}#bank-library span.move{background-image:url(../img/move.png);background-position:left center;background-repeat:no-repeat;display:block;float:left;height:32px;left:0;cursor:move !important;margin:-1px 6px 0 0;width:24px}#bank-library span.remove{cursor:pointer;float:right;width:32px;height:32px;display:block;background-image:url(../img/icons/25/delete.png);background-repeat:no-repeat;background-position:50% 50%;transition:background-color .33s}#bank-library span.remove:hover{background-color:#f29446}#bank-library span.title{display:block;float:left;height:32px;overflow:hidden;max-width:70%;white-space:nowrap}#bank-library input::selection{background:#883996 !important;color:white}#bank-library .icon-add{width:1em;height:1em;display:inline-block;font-size:inherit;vertical-align:baseline}#bank-library .icon-add i{position:absolute;width:1em;margin-top:.3em;border-top:.4em solid #888}#bank-library .icon-add b{position:absolute;height:1em;margin-left:.3em;border-left:.4em solid #888}#bank-library #bank-list{left:0;list-style:none;margin:0;padding:0;position:fixed;bottom:45px;overflow-y:auto;top:45px;width:20%;background:#2c2c2c}#bank-library #bank-list>div.add-bank{color:#fff;cursor:pointer;padding-left:5px;line-height:32px;text-transform:uppercase;transition:all .33s}#bank-library #bank-list>div.add-bank:hover{background:#f29446}#bank-library #bank-list>div.add-bank .icon-add{display:inline-block;width:24px;margin-right:6px;padding-left:5px;box-sizing:border-box}#bank-library #bank-list>div.selected span.move{background-position:right center}#bank-library #bank-list>div.selected span.remove{background-position:right center}#bank-library #bank-list .edit-bank{color:#555;width:100% !important;height:30px;margin:1px;padding-left:.5em;background-color:#fff;background-image:none;border:0 solid #ccc;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out}#bank-library #bank-list div{background:#222;color:#888;cursor:pointer;font-weight:bold;margin:0 0 1px 0;content:\002B;position:relative;transition:all .33s}#bank-library #bank-list div.selected{background:#883996;color:#fff}#bank-library #bank-list div:hover{background:#000}#bank-library #bank-list div.selected:hover{background:#600e6f}#bank-library #bank-list .bank-item{display:block;background:#222;color:#888;line-height:32px;height:32px;cursor:pointer;font-weight:bold;margin:0 0 1px 0;padding-left:5px;position:relative}#bank-library #bank-list .bank-item .remove{border-radius:0}#bank-library #bank-list .filter_{border-color:#111;border-style:dotted;border-width:0 1px 0 0;bottom:46px;left:0;padding-top:18px;position:fixed;top:0;right:0;width:240px;overflow:auto}#bank-library #bank-list .filter_ h1{font-weight:2em;text-transform:uppercase;font-weight:bold;margin:0 0 0 10px}#bank-library #bank-list .filter_ h2{color:white;font-size:15px;text-transform:uppercase;font-weight:bold;margin-top:1em;margin-left:10px}#bank-library #bank-list .filter_ .control-group{float:left;margin-right:9px;margin-left:10px}#bank-library #bank-list .filter_ .control-group label{color:#fff;display:inline-block;font-size:12px;font-weight:bold}#bank-library #bank-list .filter_ .control-group select{width:50px}#bank-library #bank-list .filter_>.form-group{padding:15px}#bank-library #bank-title{background:#111;left:20%;height:92px;margin:0;padding:0;position:fixed;text-align:left;top:45px;width:50%}#bank-library #bank-title h1{font-size:2em;padding:0;max-height:2em;display:block;margin:.5em .8em 0 .8em;padding-bottom:.2em;color:white;white-space:nowrap;overflow:hidden}#bank-library #bank-title form{margin:0 2em;color:#666}#bank-library #bank-title h2{margin:4px 0 0 14px;display:block;text-transform:initial !Important;color:white;font-size:12px;line-height:20px;text-align:left}#bank-library #bank-title small{font-size:10px;color:#bbb;text-align:left;display:block;margin-left:14px}#bank-library #bank-title select{display:block;float:left;height:22px;margin:5px 2.5%;width:20%;font-size:10px}#bank-library #bank-pedalboards{background:#111;bottom:46px;left:20%;list-style:none;margin:0;overflow-x:hidden;overflow-y:auto;padding:0 0 200px 0;position:fixed;top:164px;width:50%}#bank-library #bank-pedalboards>div{margin:0 10px 20px 20px}#bank-library #bank-pedalboards .pedalboard{color:#fff;cursor:-webkit-grab !important;cursor:grab !important;width:100%;transition:all .33s}#bank-library #bank-pedalboards .pedalboard:active{cursor:-webkit-grabbing !important;cursor:grabbing !important}#bank-library #bank-pedalboards .pedalboard:hover{background-color:#883996}#bank-library #bank-pedalboards .pedalboard .info{height:32px;padding-left:5px;background:rgba(255,255,255,0.07)}#bank-library #bank-pedalboards .pedalboard .title{display:block;line-height:32px;font-weight:bold;overflow:hidden;position:relative}#bank-library #bank-pedalboards .pedalboard .img{display:inline-block;float:left;height:auto;margin:0 9px 0 0;text-align:center;width:100%;background:url(../img/background.jpg);background-size:auto 100%;background-position:center center;box-shadow:inset 0 3px 6px black;border:1px solid #000}#bank-library #bank-pedalboards .mode{border:none;margin:22px 0 18px 0;text-align:center}#bank-library #bank-edit{position:fixed;display:block;background:#111 url(../img/watermark.png) 100% 100% no-repeat;width:80%;bottom:45px;top:45px;left:20%}#bank-library #bank-pedalboards-search{bottom:45px;right:0;overflow:auto;position:fixed;top:0;width:30%;z-index:6}#bank-library #bank-pedalboards-search h2{text-transform:uppercase;color:white;font-size:1.5em;font-weight:bold;float:left}#bank-library #bank-pedalboards-search .box{background:#111;padding:0;box-sizing:border-box}#bank-library #bank-pedalboards-search input{width:50%;padding-left:1em}#bank-library #bank-pedalboards-search .remove{display:none !important}#bank-library #bank-pedalboards-search label{color:#fff;font-weight:bold;text-transform:uppercase;float:left;line-height:45px;padding-left:10px}#bank-library #bank-pedalboards-search .control-group{margin-bottom:10px;padding:0 2%;position:relative;width:29.3%}#bank-library #bank-pedalboards-search input[type="search"]{width:50% !important;float:right;margin:5px}#bank-library #bank-pedalboards-search select{width:100%}#bank-library #bank-pedalboards-search .input-clean{background:#333;color:#fff;cursor:pointer;font-size:8px;height:18px;left:auto !important;position:absolute;right:8px !important;text-align:center;text-transform:uppercase;top:31px !important;width:18px;border-radius:100%}#bank-pedalboards-mode{background:#333;left:30%;height:30px;margin:0;padding:20px 0 0 0;position:absolute;text-align:center;top:68px;width:45%}#bank-pedalboards-mode .grid{background:url(../img/icn-grid-list.png) no-repeat left bottom;display:inline-block;height:14px;width:14px}#bank-pedalboards-mode .grid.selected{background-position:left top}#bank-pedalboards-mode .list{background:url(../img/icn-grid-list.png) no-repeat right bottom;display:inline-block;height:14px;width:14px}#bank-pedalboards-mode .list.selected{background-position:right top}.pedalboard .addressings{width:100%}.pedalboard .addressings .footswitch{background:#2b2b2b url(../img/footswitch-free.png) no-repeat center top;float:left;height:20px;width:25%}.pedalboard .addressings .footswitch.addressed{background:#2b2b2b url(../img/footswitch-addressed.png) no-repeat center top}#bank-pedalboards-result{position:fixed;bottom:45px;top:45px;overflow:auto;width:30%;background:#111}#bank-pedalboards-result .pedalboard{color:#fff;background:rgba(255,255,255,0.07);cursor:-webkit-grab;cursor:grab;margin:0 20px 20px 10px;transition:all .33s}#bank-pedalboards-result .pedalboard:active{cursor:-webkit-grabbing !important;cursor:grabbing !important}#bank-pedalboards-result .pedalboard:hover{background-color:#883996}#bank-pedalboards-result .pedalboard .img{display:block;height:auto;margin:0 auto;text-align:center;width:100%;background:url(../img/background.jpg);background-size:auto 100%;background-position:center center;box-shadow:inset 0 3px 6px black;border:1px solid #000}#bank-pedalboards-result .pedalboard .img img{max-width:100%}#bank-pedalboards-result .pedalboard .title{display:block;line-height:32px;font-weight:bold;overflow:hidden;position:relative;padding-left:5px}#bank-pedalboards-result .pedalboard .description{display:block;font-size:11px;height:27px;line-height:1.5;overflow:hidden}#file-manager-library{background:#2c2c2c url(../img/watermark.png) 100% 100% no-repeat;top:0;bottom:46px;left:0;right:0;overflow-x:hidden;overflow-y:auto;position:absolute;z-index:2}#file-manager-wrapper{display:flex;position:fixed;top:45px;bottom:55px;left:2em;right:2em;overflow:auto;padding-top:1%}#file-manager-wrapper iframe{flex:1;border:none}#pedalboards-library{background:#2c2c2c;bottom:45px;left:0;overflow-x:hidden;overflow-y:auto;position:absolute;right:0;top:0;z-index:2}#pedalboards-library h1{color:#fff;display:inline-block;font-size:24px;line-height:35px;margin:0 0 18px 0;padding:0}#pedalboards-library header .form-group{display:inline-block;float:right;margin-top:5px;margin-right:5px}#pedalboards-library header .form-group .form-control{width:200px !important}#pedalboards-library header .form-group .input-clean{line-height:20px;margin-right:12px !important}#pedalboards-library header .view-modes{float:right;margin-right:12px;cursor:pointer}#pedalboards-library header .view-modes .view-mode{font-size:130%}#pedalboards-library header .view-modes .view-mode.selected{color:#fff}#pedalboards-library .filter{border-color:#111;border-style:dotted;border-width:0 1px 0 0;bottom:45px;left:0;position:fixed;right:0;top:45px;width:240px}#pedalboards-library .filter .control-group{float:left;margin-right:9px}#pedalboards-library .filter .control-group label{color:#fff;display:inline-block;font-size:12px;font-weight:bold;width:165px}#pedalboards-library .filter .control-group select{width:50px}#pedalboards-library .pedalboards{left:0;position:fixed;right:0;top:45px;bottom:45px;overflow:auto;background:#111 url(../img/watermark.png) 100% 100% no-repeat;padding-top:1%}#pedalboards-library .pedalboards .pedalboard{color:#fff;float:left;margin:0 0 1% 1%;width:18.8%;position:relative;background-color:transparent;background-image:url(../img/background.jpg);background-position:center center;background-repeat:repeat;background-size:auto 100%;transition:all .33s;cursor:pointer}#pedalboards-library .pedalboards .pedalboard:hover .info{background-color:#883996}#pedalboards-library .pedalboards .pedalboard .img{box-shadow:inset 0 3px 6px black;border:1px solid #000;display:block;width:100%;height:10vw;overflow:hidden;text-align:center;background-image:url(../img/loading-pedalboard.gif);background-position:center center;background-repeat:no-repeat;background-size:auto;position:relative}#pedalboards-library .pedalboards .pedalboard .img.loaded{background-size:contain}#pedalboards-library .pedalboards .pedalboard .img.broken{background-image:url(../img/icons/broken_image.svg);background-size:auto 50%}#pedalboards-library .pedalboards .pedalboard.broken .img::after{content:"";position:absolute;top:0;left:0;right:0;bottom:0;z-index:10;background-color:rgba(17,17,17,0.5);background-image:url(../img/icons/broken_pedal.svg);background-size:auto 50%;background-repeat:no-repeat;background-position:center}#pedalboards-library .pedalboards .pedalboard .info{background-color:#2c2c2c;transition:background-color .33s}#pedalboards-library .pedalboards .pedalboard .title{display:block;line-height:32px;font-weight:bold;overflow:hidden;position:relative;height:32px;left:5px;width:calc(100% - 32px);text-overflow:ellipsis;white-space:nowrap}#pedalboards-library .pedalboards .pedalboard .description{display:block;font-size:12px;height:36px;line-height:1.5;overflow:hidden}#pedalboards-library .pedalboards .pedalboard .actions{font-weight:bold;text-transform:uppercase;position:absolute;top:0;right:0}#pedalboards-library .pedalboards .pedalboard .actions .js-remove{width:32px;height:32px;background-image:url(../img/icons/25/delete.png);background-repeat:no-repeat;background-position:50% 50%;transition:background-color .33s}#pedalboards-library .pedalboards .pedalboard .actions .js-remove:hover{background-color:#f29446}#pedalboards-library .pedalboards .pedalboard .actions>span{cursor:pointer}#pedalboards-library .pedalboards .pedalboard .actions>span:hover{text-decoration:underline}#pedalboards-library .pedalboards .pedalboard .actions>span.duplicate{margin:0 6px}#pedalboards-library .pedalboards.list-selected .img{display:none}#nprogress{pointer-events:none;z-index:103100}#nprogress .bar{background:#883996;position:fixed;z-index:103100;top:45px;left:0;opacity:.5;width:100%;height:2px}#nprogress .peg{display:none}#nprogress .spinner{display:none;position:fixed;z-index:1031;top:45px;right:0;background:rgba(0,0,0,0.5);padding:6px;box-sizing:border-box;transform:translate(-50% -50%)}#nprogress .spinner-icon{width:32px;height:32px;box-sizing:border-box;border:solid 3px transparent;border-top-color:#f29446;border-left-color:rgba(242,148,70,0.66);border-bottom-color:rgba(242,148,70,0.33);border-radius:50%;-webkit-animation:nprogress-spinner 400ms linear infinite;animation:nprogress-spinner 400ms linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .spinner,.nprogress-custom-parent #nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.mod-pedal-settings .mod-address.addressed,.mod-pedal-settings .preset-btn-assign-all.addressed{background-color:#a144b2}.mod-pedal-settings-address .mod-box .form-container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.mod-pedal-settings-address .mod-box .form-container .btn.js-type,.mod-pedal-settings-address .mod-box .form-container .btn.advanced-toggle{border:solid #333 1px}.mod-pedal-settings-address .mod-box .form-container .main-form-container{width:691px;padding-right:3px}.mod-pedal-settings-address .mod-box .form-container .main-form-container label{text-align:center}.mod-pedal-settings-address .mod-box .form-container .main-form-container .btn.js-type.selected{background-color:#333;color:white}.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike{display:flex;flex-basis:100%;align-items:center;color:black;margin:6px 0}.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike:before,.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike:after{content:"";flex-grow:1;background:black;height:3px;font-size:0;line-height:0}.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike:before{margin-right:8px}.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike:after{margin-left:8px}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table{width:100%}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:first-child{background-color:#f66}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(2){background-color:#ff6}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(3){background-color:#6ff}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(4){background-color:#bb7cd5}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(5){background-color:#f5a77e}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(6){background-color:#6f6}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(7){background-color:#f6f}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(8){background-color:#8080ff}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th,.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table td{border:solid #333 1px;text-align:center}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table td{padding:5px;cursor:pointer}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table td.selected{background-color:#883996;color:white;box-shadow:3px 3px 3px #999}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table td.disabled{cursor:auto;color:#999;background-color:#d9d9d9;box-shadow:inset 0 0 3px #555}.mod-pedal-settings-address .mod-box .form-container .main-form-container .midi-custom-uri{color:#883996}.mod-pedal-settings-address .mod-box .form-container .buttons-container{padding-right:3px;padding-bottom:18px}.mod-pedal-settings-address .mod-box .form-container .advanced-container{border-left:1px solid #eee;padding-left:12px;margin-left:12px}.mod-pedal-settings-address .mod-box .form-container .advanced-container .cv-op-mode label{width:100% !important}.mod-pedal-settings-address .mod-box .form-container .advanced-container .cv-op-mode .mod-controls{width:155px !important}.mod-cv-output .output-cv-checkbox{display:flex;position:absolute;top:45px;left:15px}.mod-cv-output .output-cv-checkbox .checkbox-container{position:relative;padding-left:45px;cursor:pointer;font-size:22px;height:35px;width:35px}.mod-cv-output .output-cv-checkbox .checkbox-container .checkmark{position:absolute;top:0;left:0;height:35px;width:35px;background-color:#eee}.mod-cv-output .output-cv-checkbox .checkbox-container .checkmark:after{content:"";position:absolute;display:none;left:13px;top:7px;width:10px;height:17px;border:solid white;border-width:0 4px 4px 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]{position:absolute;opacity:0;cursor:pointer;z-index:5;width:100%;height:100%;top:0;left:0}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]:disabled{cursor:not-allowed}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]:checked~.checkmark{background-color:#883996}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]:checked~.checkmark:after{display:block}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]:disabled~.checkmark{background-color:#999}.mod-cv-output .output-cv-checkbox input[type=text]{font-size:20px}.mod-cv-output .output-cv-checkbox ::selection{background:#883996 !important;color:white !important}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-semibold-webfont.eot');src:url('../fonts/cooper/cooperhewitt-semibold-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-semibold-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-semibold-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-semibold-webfont.ttf') format('truetype');font-weight:600;font-style:normal;font-display:swap}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-light-webfont.eot');src:url('../fonts/cooper/cooperhewitt-light-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-light-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-light-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-light-webfont.ttf') format('truetype');font-weight:300;font-style:normal;font-display:swap}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-lightitalic-webfont.eot');src:url('../fonts/cooper/cooperhewitt-lightitalic-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-lightitalic-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-lightitalic-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-lightitalic-webfont.ttf') format('truetype');font-weight:300;font-style:italic;font-display:swap}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-bookitalic-webfont.eot');src:url('../fonts/cooper/cooperhewitt-bookitalic-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-bookitalic-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-bookitalic-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-bookitalic-webfont.ttf') format('truetype');font-weight:400;font-style:italic;font-display:swap}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-book-webfont.eot');src:url('../fonts/cooper/cooperhewitt-book-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-book-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-book-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-book-webfont.ttf') format('truetype');font-weight:400;font-style:normal;font-display:swap}*{outline:0 !important}html,body{height:100%;overflow:hidden}body{background:#111;min-width:1024px}body.ios{position:fixed;min-width:980px}*{font-family:"cooper hewitt",Sans-serif !important}.unpadded{padding:0 !important}.unpadded-r{padding:0 0 0 7px}.unpadded-l{padding:0 7px 0 0}.ui-draggable:active{cursor:-webkit-grabbing !important;cursor:grabbing !important}.form-control{width:100% !important}#wrapper{bottom:0;left:0;min-width:980px;overflow:hidden;position:absolute;right:0;top:0}hr.dotted{border:1px dotted #444;margin:2px 0 3px 0}.preset-list{width:100%;height:200px !important}.input-clean{background:#333;color:#fff;cursor:pointer;height:18px;left:auto !important;position:absolute;right:0;margin-right:18px !important;text-align:center;text-transform:uppercase;margin-top:-26px !important;width:18px;border-radius:100%}.input-clean:after{content:"×";content:"\00d7";font-size:14px;font-weight:bold}.form-control::selection{background:#883996 !important;color:white !important}#notifications{position:absolute;top:55px;right:0;width:320px;z-index:100000}#notifications>section{border-radius:0;margin-bottom:0 !important;opacity:.96;cursor:pointer}#notifications .progressbar{border:0;height:8px;overflow:hidden;position:absolute;left:8px;bottom:0;width:306px}#notifications .progressbar .progressbar-value{background-color:rgba(136,57,150,0.7);height:90%;margin:0}#main-menu{background:#111;bottom:0;color:#fff;height:45px;left:0;position:absolute;right:0;z-index:10}#main-menu>div{cursor:pointer;float:left}#main-menu>div.icon{height:45px;width:45px}#main-menu>div.icon>img{margin:5px 0 0 6px}#main-menu>div.icon.selected{background:#000}#main-menu>div.icon>a{display:block;height:45px;width:45px}#main-menu #mod-plugins{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-45px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-plugins:hover{background-color:#000;background-position:-45px bottom}#main-menu #mod-plugins.selected{background-color:#000;background-position:-45px bottom}#main-menu #mod-cloud-plugins{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-315px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-cloud-plugins:hover{background-color:#000;background-position:-315px bottom}#main-menu #mod-cloud-plugins.selected{background-color:#000;background-position:-315px bottom}#main-menu #mod-pedalboard{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-90px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-pedalboard:hover{background-color:#000;background-position:-90px bottom}#main-menu #mod-pedalboard.selected{background-color:#000;background-position:-90px bottom}#main-menu #mod-bank{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-135px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-bank:hover{background-color:#000;background-position:-135px bottom}#main-menu #mod-bank.selected{background-color:#000;background-position:-135px bottom}#main-menu #mod-file-manager{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-180px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-file-manager:hover{background-color:#000;background-position:-180px bottom}#main-menu #mod-file-manager.selected{background-color:#000;background-position:-180px bottom}#main-menu #banks-saving{margin:21px 0 0 6px;font-size:10px;color:#666;display:none}#main-menu #mod-show-midi-port,#main-menu #mod-transport-icon,#main-menu .mod-bypass,#main-menu #mod-cpu,#main-menu #mod-ram,#main-menu #mod-xruns,#main-menu #mod-buffersize,#main-menu #mod-cpu-stats{background-position:8px 50%;background-repeat:no-repeat;padding-left:40px;padding-right:12px;color:#fff;float:right;font-size:11px;font-weight:bold;line-height:4.5;text-transform:uppercase;width:auto !important;transition:all .33s}#main-menu #mod-show-midi-port:hover,#main-menu #mod-transport-icon:hover,#main-menu .mod-bypass:hover,#main-menu #mod-cpu:hover,#main-menu #mod-ram:hover,#main-menu #mod-xruns:hover,#main-menu #mod-buffersize:hover,#main-menu #mod-cpu-stats:hover{background-color:#000}#main-menu #mod-show-midi-port.selected,#main-menu #mod-transport-icon.selected,#main-menu .mod-bypass.selected,#main-menu #mod-cpu.selected,#main-menu #mod-ram.selected,#main-menu #mod-xruns.selected,#main-menu #mod-buffersize.selected,#main-menu #mod-cpu-stats.selected{background-color:#333}#main-menu #mod-show-midi-port{background-image:url(../img/icons/25/midi.png)}#main-menu #mod-transport-icon{background-image:url(../img/icons/25/stop.png);font-family:monospace;width:10.5em !important;text-align:right}#main-menu #mod-transport-icon.playing{background-image:url(../img/icons/25/transport.png)}#main-menu .mod-bypass{background-image:url(../img/icons/25/bypass.png)}#main-menu .mod-bypass.bypassed{background-color:#883996}#main-menu #mod-cpu,#main-menu #mod-ram{cursor:default;width:115px !important;padding:0 !important;background:linear-gradient(to right, #1e5799 0, #2989d8 69%, #ff6430 78%, #ff6430 93%, #c40000 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#1e5799', endColorstr='#c40000', GradientType=1)}#main-menu #mod-cpu .progress-title{background-image:url(../img/icons/25/cpu.png);background-position:0 50%;background-repeat:no-repeat;padding-left:30px}#main-menu #mod-ram .progress-title{background-image:url(../img/icons/25/ram.png);background-position:0 50%;background-repeat:no-repeat;padding-left:30px}#main-menu #mod-xruns{background-image:url(../img/icons/25/cpu.png);width:115px !important}#main-menu #mod-cpu-stats{background-image:url(../img/icons/25/cpu.png);width:155px !important;text-transform:none}#main-menu #mod-buffersize{background-image:url(../img/icons/25/cpu.png);width:125px !important}#main-menu .progress{height:45px;margin-bottom:0;overflow:hidden;float:right;background-color:#111;-webkit-transition:width 1s;transition:width 1s}#main-menu .progress .bar{float:right;background:#222 !important}#main-menu .progress-title{position:absolute;top:0;height:45px;margin:0;margin-left:1em;color:white}#main-menu .status{background-color:#111;background-position:3px 50%;background-size:36px;background-repeat:no-repeat;border-color:#2b2b2b;border-style:solid;border-width:0 0 0 1px;float:right}#main-menu #mod-settings{font-family:fontello !important;font-size:28px;padding-top:3px;padding-left:10px;position:relative;color:#fff;opacity:.66;cursor:pointer;transition:all .33s}#main-menu #mod-settings:after{content:'\e802'}#main-menu #mod-settings:hover{opacity:1}#main-menu #mod-update{background-image:url(../img/icons/36/update.png)}#main-menu #mod-update.uptodate{background-image:url(../img/icons/36/ok.png)}#main-menu #mod-status{background-image:url(../img/icons/36/disconnect.png)}#main-menu #mod-status.online{background-image:url(../img/icons/36/disconnect.png)}#main-menu #mod-devices{background-image:url(../img/icons/36/control-chain.png)}#cloud_install_all{background-image:url(../img/icons/36/save.png);float:right;font-size:21px}#cloud_update_all{background-image:url(../img/icons/36/update.png);float:right;font-size:21px}header#pedalboard-actions,#bank-library header,#cloud-plugins-library header,#pedalboards-library header,#file-manager-library header{display:block;left:0;height:45px;line-height:45px;position:fixed;right:0;top:0;box-shadow:0 1px 10px rgba(0,0,0,0.1);z-index:5;background-color:#111;background-position:3px 50%;background-size:36px;background-repeat:no-repeat;background-image:url(../img/icons/36/mod.png);padding-left:45px}header#pedalboard-actions .help,#bank-library header .help,#cloud-plugins-library header .help,#pedalboards-library header .help,#file-manager-library header .help{background-image:url(../img/icn-help-16x16.png);background-position:center center;background-repeat:no-repeat;display:inline-block;height:21px;width:21px}header#pedalboard-actions .close,#bank-library header .close,#cloud-plugins-library header .close,#pedalboards-library header .close,#file-manager-library header .close{opacity:1;position:absolute;right:10px;top:16px}header#pedalboard-actions .drop-menu,#bank-library header .drop-menu,#cloud-plugins-library header .drop-menu,#pedalboards-library header .drop-menu,#file-manager-library header .drop-menu{position:absolute;display:none;list-style-type:none;padding:0;background-color:rgba(0,0,0,0.8);color:#999;width:150px}header#pedalboard-actions .drop-menu:hover,#bank-library header .drop-menu:hover,#cloud-plugins-library header .drop-menu:hover,#pedalboards-library header .drop-menu:hover,#file-manager-library header .drop-menu:hover{display:block}header#pedalboard-actions .drop-menu li,#bank-library header .drop-menu li,#cloud-plugins-library header .drop-menu li,#pedalboards-library header .drop-menu li,#file-manager-library header .drop-menu li{padding:5px 20px}header#pedalboard-actions .drop-menu li:hover,#bank-library header .drop-menu li:hover,#cloud-plugins-library header .drop-menu li:hover,#pedalboards-library header .drop-menu li:hover,#file-manager-library header .drop-menu li:hover{color:white;background-color:#883996}header#pedalboard-actions .drop-menu .sub-menu,#bank-library header .drop-menu .sub-menu,#cloud-plugins-library header .drop-menu .sub-menu,#pedalboards-library header .drop-menu .sub-menu,#file-manager-library header .drop-menu .sub-menu{display:none;list-style-type:none;padding:0;position:absolute;left:150px;height:14em;overflow-y:auto;overflow-x:hidden;margin-top:-50px;background-color:rgba(0,0,0,0.8);width:15em;border-left:3px solid #666}header#pedalboard-actions .drop-menu .sub-menu:hover,#bank-library header .drop-menu .sub-menu:hover,#cloud-plugins-library header .drop-menu .sub-menu:hover,#pedalboards-library header .drop-menu .sub-menu:hover,#file-manager-library header .drop-menu .sub-menu:hover{display:block}header#pedalboard-actions .menu-trigger:hover+.drop-menu,#bank-library header .menu-trigger:hover+.drop-menu,#cloud-plugins-library header .menu-trigger:hover+.drop-menu,#pedalboards-library header .menu-trigger:hover+.drop-menu,#file-manager-library header .menu-trigger:hover+.drop-menu{display:block}header#pedalboard-actions .sub-menu-trigger:hover ul,#bank-library header .sub-menu-trigger:hover ul,#cloud-plugins-library header .sub-menu-trigger:hover ul,#pedalboards-library header .sub-menu-trigger:hover ul,#file-manager-library header .sub-menu-trigger:hover ul{display:block}header#pedalboard-actions button,#bank-library header button,#cloud-plugins-library header button,#pedalboards-library header button,#file-manager-library header button{background-position:8px 50%;background-size:25px;background-repeat:no-repeat;padding:0 12px 0 40px;background-color:transparent;border:none;height:45px;line-height:45px;opacity:.66;color:white;transition:all .33s}header#pedalboard-actions button:hover,#bank-library header button:hover,#cloud-plugins-library header button:hover,#pedalboards-library header button:hover,#file-manager-library header button:hover,header#pedalboard-actions button.selected,#bank-library header button.selected,#cloud-plugins-library header button.selected,#pedalboards-library header button.selected,#file-manager-library header button.selected{opacity:1;background-color:black}header#pedalboard-actions button.js-save,#bank-library header button.js-save,#cloud-plugins-library header button.js-save,#pedalboards-library header button.js-save,#file-manager-library header button.js-save{background-image:url(../img/icons/25/save.png)}header#pedalboard-actions button.js-save-as,#bank-library header button.js-save-as,#cloud-plugins-library header button.js-save-as,#pedalboards-library header button.js-save-as,#file-manager-library header button.js-save-as{background-image:url(../img/icons/25/save.png)}header#pedalboard-actions button.js-preset,#bank-library header button.js-preset,#cloud-plugins-library header button.js-preset,#pedalboards-library header button.js-preset,#file-manager-library header button.js-preset{background-image:url(../img/icons/36/presets.png);background-size:30px 30px}header#pedalboard-actions button#js-preset-enabler:hover,#bank-library header button#js-preset-enabler:hover,#cloud-plugins-library header button#js-preset-enabler:hover,#pedalboards-library header button#js-preset-enabler:hover,#file-manager-library header button#js-preset-enabler:hover,header#pedalboard-actions button#js-preset-enabler.selected,#bank-library header button#js-preset-enabler.selected,#cloud-plugins-library header button#js-preset-enabler.selected,#pedalboards-library header button#js-preset-enabler.selected,#file-manager-library header button#js-preset-enabler.selected{background-color:#883996}header#pedalboard-actions button.js-reset,#bank-library header button.js-reset,#cloud-plugins-library header button.js-reset,#pedalboards-library header button.js-reset,#file-manager-library header button.js-reset{background-image:url(../img/icons/25/new.png)}header#pedalboard-actions button.js-reset:hover,#bank-library header button.js-reset:hover,#cloud-plugins-library header button.js-reset:hover,#pedalboards-library header button.js-reset:hover,#file-manager-library header button.js-reset:hover,header#pedalboard-actions button.js-reset.selected,#bank-library header button.js-reset.selected,#cloud-plugins-library header button.js-reset.selected,#pedalboards-library header button.js-reset.selected,#file-manager-library header button.js-reset.selected{background-color:#883996}header#pedalboard-actions button.js-cv-addressing,#bank-library header button.js-cv-addressing,#cloud-plugins-library header button.js-cv-addressing,#pedalboards-library header button.js-cv-addressing,#file-manager-library header button.js-cv-addressing{background-image:url(../img/icons/25/cv.png)}header#pedalboard-actions button.js-cv-addressing:hover,#bank-library header button.js-cv-addressing:hover,#cloud-plugins-library header button.js-cv-addressing:hover,#pedalboards-library header button.js-cv-addressing:hover,#file-manager-library header button.js-cv-addressing:hover,header#pedalboard-actions button.js-cv-addressing.selected,#bank-library header button.js-cv-addressing.selected,#cloud-plugins-library header button.js-cv-addressing.selected,#pedalboards-library header button.js-cv-addressing.selected,#file-manager-library header button.js-cv-addressing.selected{background-color:#883996}header#pedalboard-actions button.js-cloud,#bank-library header button.js-cloud,#cloud-plugins-library header button.js-cloud,#pedalboards-library header button.js-cloud,#file-manager-library header button.js-cloud{background-image:url(../img/icn-share.png);background-size:32px;background-position:5px center}header#pedalboard-actions h1,#bank-library header h1,#cloud-plugins-library header h1,#pedalboards-library header h1,#file-manager-library header h1{font-weight:2em;text-transform:uppercase;font-weight:normal;color:#999;display:inline-block;font-size:24px;line-height:49px;height:45px;padding:0}header#pedalboard-actions{box-shadow:0 2px 12px black}.tooltip{background:white;bottom:50px;height:auto;padding:7px;position:absolute}.tooltip .text{color:#222;font-weight:bold;line-height:1;text-align:center}.tooltip .arrow{border-bottom:9px solid transparent;border-left:9px solid transparent;border-right:9px solid transparent;border-top:9px solid white;height:0;position:absolute;bottom:-18px;right:14px;width:0}#mod-devices-window,#mod-upgrade{background:white;color:black;font-size:15px;max-height:350px;overflow:auto;position:absolute;right:10px;bottom:50px;z-index:100}#mod-devices-window .progressbar-wrapper,#mod-upgrade .progressbar-wrapper{border:1px solid #444;height:20px;width:330px}#mod-devices-window .progressbar,#mod-upgrade .progressbar{background-color:#88f;height:18px}#mod-devices-window{max-width:600px;min-width:500px}#mod-upgrade{max-width:450px;min-width:350px}#mod-transport-window{bottom:48px;right:310px;top:initial;left:initial;background:rgba(17,17,17,0.9);padding:50px 40px 15px 40px;z-index:999}#mod-transport-window .mod-midi-icon{width:auto !important;padding-left:25px;background-position:5% 45% !important;background-image:url(../img/icons/25/midi.png);background-size:15px;background-repeat:no-repeat}#plugins-library{bottom:45px;height:166px;left:0;position:absolute;right:0;transition:all .33s;transition:opacity .5s;transition:height .5s}#plugins-library.folded{height:38px}#plugins-library.folded #plugins-library-settings-window{bottom:0}#plugins-library.fade{opacity:.2}#plugins-library .fold{cursor:pointer;display:block !important;float:right;height:36px;position:absolute;top:0;right:0;width:36px;background-color:rgba(17,17,17,0.66);background-image:url(../img/icons/25/pinned.png);background-position:center center;background-repeat:no-repeat}#plugins-library .folded{cursor:pointer;display:block;float:right;height:36px;position:absolute;top:0;right:0;width:36px}#plugins-library.auto .fold{background-image:url(../img/icons/25/autohide.png)}#plugins-library .settings{background-color:rgba(17,17,17,0.66);background-image:url(../img/icn-search-white.png);background-position:center center;background-repeat:no-repeat;cursor:pointer;display:block;height:36px;position:absolute;right:37px;top:0;width:36px}#plugins-library #plugins-library-settings-window{background-color:rgba(17,17,17,0.66);bottom:130px;color:#fff;position:absolute;right:74px;transition:bottom .5s}#plugins-library #plugins-library-settings-window h1{padding:0}#plugins-library #plugins-library-settings-window form{padding:4px;border:0;margin:0}#plugins-library #plugins-library-settings-window label{margin-right:10px}#plugins-library #plugins-library-settings-window label.checkbox:last-child{margin-right:0}#plugins-library #plugins-library-settings-window .control-group{position:relative}#plugins-library #plugins-library-settings-window input{width:120px;height:28px;line-height:28px;padding:0 6px;outline:0;border:0}#plugins-library #plugins-library-settings-window .input-clean{position:absolute;top:50%;right:8px;bottom:auto;left:auto;transform:translateY(-50%);margin:0 !important;border-radius:100%}#plugins-library ul{list-style:none;margin:6px 73px 0 0;overflow:hidden;padding:10px 0 0 0;position:relative;width:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}#plugins-library ul li{background:rgba(17,17,17,0.66);color:#bbb;cursor:pointer !important;display:inline-block;float:left;font-size:11px;margin:0 1px 0 0;padding:3px 11px;text-shadow:1px 1px 0 #000;text-transform:uppercase;border-top:solid 3px transparent;transition-property:border-radius,color,margin-top,padding,border-top;transition-duration:.33s}#plugins-library ul li:hover{margin-top:-6px;padding:6px 11px;color:white}#plugins-library ul li.selected{color:white;font-weight:bold;margin-top:-6px;padding:6px 11px;border-top:solid 3px #883996}#plugins-library.scrolling .plugins-wrapper{transition:all .1s !important}#plugins-library #plugins-list{background:rgba(17,17,17,0.66);height:140px;overflow:hidden;position:relative;width:100%;z-index:1}#plugins-library #plugins-list .nav{background:url(../img/nav-left.png) 50% 50% no-repeat;cursor:pointer;position:absolute;top:0;width:50px;height:100%;opacity:.66}#plugins-library #plugins-list .nav:hover{opacity:1}#plugins-library #plugins-list .nav-left{left:0}#plugins-library #plugins-list .nav-right{background:url(../img/nav-right.png) 50% 50% no-repeat;right:0}#plugins-library #plugins-list #plugins-results{bottom:0;left:50px;position:absolute;overflow:hidden;right:50px;top:0}#plugins-library #plugins-list .plugins-wrapper{position:absolute;display:inline-block;white-space:nowrap;transition:all .5s;top:120px;left:0;opacity:0}#plugins-library #plugins-list .plugins-wrapper.selected{top:0;opacity:1}#plugins-library #plugins-list .plugins-wrapper .plugin{color:#fff;font-size:11px;font-weight:bold;height:108px;padding:14px 12px 10px;position:relative;text-align:center;display:inline-block}#plugins-library #plugins-list .plugins-wrapper .plugin .thumb{height:67px;cursor:pointer !important;position:relative;display:flex;flex-direction:column;justify-content:center}#plugins-library #plugins-list .plugins-wrapper .plugin .thumb:active{cursor:-webkit-grabbing !important;cursor:grabbing !important}#plugins-library #plugins-list .plugins-wrapper .plugin .thumb img{height:auto;margin:0 auto 3px;width:auto;pointer-events:none;max-width:256px;max-height:64px}#plugins-library #plugins-list .plugins-wrapper .plugin .status{display:block;height:16px;margin:0 auto;width:16px}#plugins-library #plugins-list .plugins-wrapper .plugin .status.demo{display:inline-block;width:auto;height:auto;margin:0}#plugins-library #plugins-list .plugins-wrapper .plugin .status.installed{background-image:url(../img/icn-installed.png);background-position:center center;background-repeat:no-repeat}#plugins-library #plugins-list .plugins-wrapper .plugin .status.outdated{background-image:url(../img/icn-outdated.png);background-position:center center;background-repeat:no-repeat}#plugins-library #plugins-list .plugins-wrapper .plugin .status.blocked{background-image:url(../img/icn-blocked.png);background-position:center center;background-repeat:no-repeat}#plugins-library #plugins-list .plugins-wrapper .plugin .author{display:block;line-height:1.5}#plugins-library #plugins-list .plugins-wrapper .plugin .title{display:block;line-height:1.25}#plugins-library #plugins-list .plugins-wrapper .plugin .rating{background-image:url(../img/icn-rating.png);background-position:center top;background-repeat:no-repeat;display:block;height:16px;width:132px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.one{background-position:center -15px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.two{background-position:center -30px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.three{background-position:center -45px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.four{background-position:center -60px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.five{background-position:center -75px}#plugins-library #plugins-list .plugins-wrapper .plugin .downloads{display:block}#plugins-library #plugins-list .plugins-wrapper .plugin .demo-container{position:absolute;left:0;bottom:3px;right:0;text-align:center}.plugin-info{background-color:rgba(20,20,20,0.9);bottom:0;color:#ccc;left:0;overflow-y:scroll;position:fixed;right:0;top:0;z-index:40}.plugin-info .box{margin-top:3em;margin-bottom:3em;background-color:#111;padding:15px;border:2px solid #333}.plugin-info .box .row-fluid{position:relative}.plugin-info .close{color:#fff;font-size:12px;font-weight:bold;opacity:1;position:absolute;right:10px;text-shadow:none;text-transform:uppercase;top:10px}.plugin-info .screenshot{padding:18px;min-width:3em;min-height:9em;display:inline-block;z-index:0}.plugin-info .screenshot div.beta{background-color:#ff2927;font-family:"cooper hewitt",sans-serif;font-weight:700;padding:5px 10px;color:white;position:absolute;top:18px;left:30px;font-size:16px}.plugin-info figure{position:relative;min-height:1px;padding-right:15px;padding-left:15px}@media (min-width:992px){.plugin-info figure{float:left;width:33.33333333%}}.plugin-info figure:hover{z-index:10 !important}.plugin-info .plugin-description{position:relative;min-height:1px;padding-right:15px;padding-left:15px;height:100%;display:block;position:a;padding-left:45px;background:-moz-linear-gradient(left, rgba(17,17,17,0.1) 1em, #111 2em);background:-webkit-gradient(linear, left top, right top, color-stop(1em, rgba(17,17,17,0.1)), color-stop(2em, #111));background:-webkit-linear-gradient(left, rgba(17,17,17,0.1) 1em, #111 2em);background:-o-linear-gradient(left, rgba(17,17,17,0.1) 1em, #111 2em);background:-ms-linear-gradient(left, rgba(17,17,17,0.1) 1em, #111 2em);background:linear-gradient(to right, rgba(17,17,17,0.1) 1em, #111 2em);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffffff', endColorstr='#ffffff', GradientType=1)}@media (min-width:992px){.plugin-info .plugin-description{float:left;width:58.33333333%}}.plugin-info .plugin-description>p{font-size:16px;margin-bottom:27px;white-space:pre-wrap}.plugin-info header{padding:18px 0 0}.plugin-info header h1{display:inline-block;vertical-align:middle;color:#fff;font-size:27px;font-weight:bold;line-height:1;margin:0;padding:0}.plugin-info header h1 span{font-size:16px;line-height:1;margin:0;padding:0}.plugin-info .version{color:#ccc;font-size:12px;font-weight:bold;text-transform:uppercase}.plugin-info .version span.bold{color:#fff;font-size:14px}.plugin-info .version button{font-weight:bold;margin-top:6px;text-transform:uppercase}.plugin-info .rating{background:url(../img/icn-rating.png) no-repeat -12px top;display:inline-block;height:14px;width:84px}.plugin-info .rating.one{background:url(../img/icn-rating.png) no-repeat -12px -15px}.plugin-info .rating.two{background:url(../img/icn-rating.png) no-repeat -12px -30px}.plugin-info .rating.three{background:url(../img/icn-rating.png) no-repeat -12px -45px}.plugin-info .rating.four{background:url(../img/icn-rating.png) no-repeat -12px -60px}.plugin-info .rating.five{background:url(../img/icn-rating.png) no-repeat -12px -75px}.plugin-info .rating-label{color:#fff;font-size:12px;font-weight:bold;text-transform:uppercase}.plugin-info .my-rate{width:15px;height:15px;margin:0;padding:0;float:left;cursor:pointer}.plugin-info .plugin-specs{text-align:center;width:100%;border-color:#333}.plugin-info .plugin-specs th{background:#333;color:#fff;border-color:#333;text-align:center}.plugin-info .plugin-specs td{border-color:#333}.plugin-info .plugin-specs td:first-child{width:40%}.plugin-info .plugin-specs td:not(first-child){width:20%}.plugin-info .comments-reviews{border-top:1px solid #333;margin-top:10px;padding-top:10px}.plugin-info .comments-reviews h1{color:#fff;font-size:21px;line-height:2;margin:0;padding:0}.plugin-info .reviews{border-bottom:1px solid #333;margin:9px 0 18px 0;padding:9px 0 18px 0}.plugin-info article.review{margin:18px 0}.plugin-info article.review h1{color:#fff;font-size:14px}.plugin-info article.review h1 .rating{margin-left:6px}.plugin-info article.review h1 .rating-label{color:#ccc;display:inline;font-size:12px;font-weight:normal;margin-left:6px}.plugin-info article.review .bar{background:#333;display:block;height:12px;margin:2px 6px 0;position:relative;width:100px}.plugin-info article.review .progress-bar{background:#444;bottom:0;display:block;height:10px;left:0;margin:1px;position:absolute;right:0;top:0;width:50%}.plugin-info article.review .rating-number{display:block;font-size:12px;margin-top:-1px}.plugin-info article.comment{border-top:1px solid #333;margin:18px 0 0;padding:18px 0 0}.plugin-info article.comment h1{color:#fff;font-size:16px;line-height:1}.plugin-info article.comment h1 .date{color:#ccc;font-size:12px;font-weight:normal;margin-left:6px}.plugin-info .comments{margin:18px 0;padding:18px 0}.plugin-info .comments button{margin-top:18px}.plugin-info form.comment-form{border-top:1px solid #333;margin:18px 0;padding:18px 0}.plugin-info form.comment-form label{font-size:12px;font-weight:bold;text-transform:uppercase}.plugin-info form.comment-form textarea{height:180px}.plugin-info .favorite-button{display:inline-block;vertical-align:middle;width:30px;height:30px;background:url(../img/icons/25/star-border.png) no-repeat center center;cursor:pointer;opacity:.5}.plugin-info .favorite-button:hover{opacity:.8}.plugin-info .favorite-button.favorite{opacity:1;background:url(../img/icons/25/star.png) no-repeat center center}.plugin-info .favorite-button.favorite:hover{opacity:1}.plugin-info .online-button-href{display:inline-block;margin-bottom:10px}.plugin-info .status.demo{display:inline-block;padding:4px 8px;margin:2px 0;font-size:.8em}.clearfix:after,.container:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after{content:"\0020";display:block;height:0;clear:both;visibility:hidden;overflow:hidden}.clearfix,.container,.dl-horizontal dd,.container,.container-fluid,.row,.form-horizontal .form-group,.btn-toolbar,.btn-group-vertical>.btn-group,.nav,.navbar,.navbar-header,.navbar-collapse,.pager,.panel-body{display:block}.clear{clear:both}.blend{opacity:.25}.box{padding:10px}.alignleft{float:left}.alignright{float:right}.textcenter{text-align:center}.textright{text-align:right}.bottom{margin-bottom:0 !important;padding-bottom:0 !important}.left{margin-left:0 !important;padding-left:0 !important}.right{margin-right:0 !important;padding-right:0 !important}.top{margin-top:0 !important;padding-top:0 !important}.nobullets{list-style:none}.mod-hidden{display:none}.mod-init-hidden{opacity:0}::selection{background:transparent}.mod-box{background:white;left:50%;margin:-150px -168px;padding:18px;position:absolute;top:50%;width:300px}.mod-box:not(#midi-ports-box) label{display:inline-block;float:left;line-height:2;width:96px}.mod-box .mod-controls{float:left}.mod-box input[type="text"]{width:190px}.mod-box input[type="password"]{width:190px}.blocker{background:rgba(17,17,17,0.9);color:white;display:block;position:fixed;width:100% !important;height:100% !important;top:0;left:0;text-align:center;z-index:99999}.blocker p{position:absolute;top:50%;width:100%;margin-top:56px;font-size:24px}.blocker.screen-disconnected{background:rgba(17,17,17,0.9) url(../img/icons/blocked.svg) 50% 50% no-repeat;background-size:100px 100px}.blocker.screen-disconnected .button{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);margin-top:120px}.plugin-wait{background:url(../img/loading-effect.gif) no-repeat center center;border:1px solid #ccc}.button{background:#883996;color:white;display:inline-block;cursor:pointer;line-height:25px;padding:5px 15px;transition:all .33s;border:none}.button:hover{background-color:#f29446;box-shadow:0 1px 5px black}.button.icon{padding-left:40px;background-position:10px 50%;background-repeat:no-repeat;background-size:25px}.drag-handle{bottom:0;cursor:-webkit-grabbing !important;cursor:grabbing !important;left:0;position:absolute;right:0;top:0;z-index:20}.demo-plugin{background:url("/img/trial.svg") no-repeat center center;background-size:contain;opacity:.4;z-index:11 !important}.demo-mask{width:100%;height:100%;left:0;right:0;top:0;bottom:0;position:absolute}.demo-plugin-light{opacity:.1 !important}.actions{z-index:20}@keyframes flashlight{0%{box-shadow:0 0 0 transparent}50%{box-shadow:0 0 10px white}100%{box-shadow:0 0 0 transparent}}.alert{background:rgba(17,17,17,0.9);border-width:0 0 0 6px;border-color:#883996;color:white;box-shadow:0 3px 10px black;margin-top:5px !important}.alert .js-close{color:white}.alert-warning{border-color:#f29446}.alert-danger{border-color:#c00}#go-back{width:200px;text-align:left;margin:20px 0 0 0;background:none;border:none;font-size:15px;padding:5px;color:#fff !important}#go-back span{font-family:fontello !important}#go-back span:before{content:'\e804'}#go-back:hover{text-decoration:none}.unstable-warning{float:left;background-color:#ffebb1;padding:5px;border-radius:10px} +/* TONE3000 */ +#main-menu #mod-tone3000{background-image:url(../img/tone3000-icon.png);background-position:center center;background-size:30px 30px;background-repeat:no-repeat;transition:all .33s} +#main-menu #mod-tone3000:hover{background-color:#000} +#main-menu #mod-tone3000.selected{background-color:#000} +#tone3000-library{background:#2c2c2c url(../img/watermark.png) 100% 100% no-repeat;top:0;bottom:46px;left:0;right:0;overflow-x:hidden;overflow-y:auto;position:absolute;z-index:2} +#tone3000-library header{display:block;left:0;height:45px;line-height:45px;position:fixed;right:0;top:0;box-shadow:0 1px 10px rgba(0,0,0,0.1);z-index:5;background-color:#111;background-position:3px 50%;background-size:36px;background-repeat:no-repeat;background-image:url(../img/icons/36/mod.png);padding-left:45px} +#tone3000-library header h1{font-weight:2em;text-transform:uppercase;font-weight:normal;color:#999;display:inline-block;font-size:24px;line-height:49px;height:45px;padding:0} +#tone3000-wrapper{position:absolute;top:45px;bottom:46px;left:0;right:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:#ccc} +#tone3000-logo{width:96px;height:96px} +#tone3000-wrapper h2{color:#fff;font-size:24px;font-weight:normal;text-transform:uppercase;margin:.8em 0 .2em} +#tone3000-wrapper .tone3000-hint{font-size:13px;line-height:1.5;color:#999;max-width:34em;margin:0 0 1em} +#tone3000-wrapper .tone3000-hint b{color:#ccc;font-weight:normal} +#tone3000-wrapper .tone3000-hint:last-of-type{margin-bottom:1.5em} +#tone3000-browse{background:#883996;border:none;border-radius:3px;color:#fff;cursor:pointer;font-size:14px;padding:.7em 1.4em} +#tone3000-browse:hover{background:#9d47ad} +#tone3000-wrapper .tone3000-autoopen{font-size:12px;color:#999;cursor:pointer;margin-top:1.5em} +#tone3000-wrapper .tone3000-autoopen input{margin-right:.5em;vertical-align:middle;cursor:pointer} +#tone3000-wrapper .tone3000-autoopen:hover{color:#ccc} diff --git a/html/img/tone3000-icon.png b/html/img/tone3000-icon.png new file mode 100644 index 000000000..e5615d8c8 Binary files /dev/null and b/html/img/tone3000-icon.png differ diff --git a/html/index.html b/html/index.html index 3a7606ab3..1042630ed 100644 --- a/html/index.html +++ b/html/index.html @@ -66,6 +66,8 @@ PEDALBOARDS_LABS_URL = '{{pedalboards_labs_url}}' CONTROLCHAIN_URL = '{{controlchain_url}}' LV2_PLUGIN_DIR = '{{lv2_plugin_dir}}' + TONE3000_CLIENT_ID = '{{tone3000_client_id}}' + TONE3000_API = '{{tone3000_api}}' VERSION = '{{version}}' BIN_COMPAT = '{{bin_compat}}' PLATFORM = '{{platform}}' @@ -138,6 +140,8 @@ bankList: $('#bank-list'), fileManagerBox: $('#file-manager-library'), fileManagerBoxTrigger: $('#main-menu #mod-file-manager'), + tone3000Box: $('#tone3000-library'), + tone3000BoxTrigger: $('#main-menu #mod-tone3000'), shareButton: $('#pedalboard-info .js-cloud'), shareWindow: $('#share-window'), presetSaveBox: $('#save-preset'), @@ -294,6 +298,7 @@ + @@ -709,6 +714,7 @@

Control Chain Device Update

+
Auto saving banks...
@@ -1017,6 +1023,35 @@

File Manager

+ +
+
+

Tone3000

+
+ +
+ +

Tone3000

+

+ Browse the TONE3000 catalog and download the tones you like. Each tone is + saved to its own folder under NAM Models, where you will find it in + the File Manager. +

+

+ A tone you just downloaded goes to the top of the model list of every NAM + plugin on the board, so you can load it without hunting for it. +

+ + +
+
+ +
diff --git a/html/js/desktop.js b/html/js/desktop.js index 631a98bdd..08e7b8abd 100644 --- a/html/js/desktop.js +++ b/html/js/desktop.js @@ -37,6 +37,8 @@ function Desktop(elements) { pedalboardTrigger: $('
'), fileManagerBox: $('
'), fileManagerBoxTrigger: $('
'), + tone3000Box: $('
'), + tone3000BoxTrigger: $('
'), pedalboardBox: $('
'), pedalboardBoxTrigger: $('
'), bankBox: $('
'), @@ -770,6 +772,8 @@ function Desktop(elements) { elements.bankBoxTrigger) this.fileManagerBox = self.makeFileManagerBox(elements.fileManagerBox, elements.fileManagerBoxTrigger) + this.tone3000Box = self.makeTone3000Box(elements.tone3000Box, + elements.tone3000BoxTrigger) this.getPluginsData = function (uris, callback) { $.ajax({ @@ -1313,6 +1317,7 @@ function Desktop(elements) { elements.bankBoxTrigger.statusTooltip() elements.cloudPluginBoxTrigger.statusTooltip() elements.fileManagerBoxTrigger.statusTooltip() + elements.tone3000BoxTrigger.statusTooltip() this.upgradeWindow = elements.upgradeWindow.upgradeWindow({ icon: elements.upgradeIcon, @@ -1810,6 +1815,14 @@ Desktop.prototype.makeFileManagerBox = function (el, trigger) { }) } +Desktop.prototype.makeTone3000Box = function (el, trigger) { + var self = this + el.tone3000Box({ + trigger: trigger, + windowManager: this.windowManager, + }) +} + Desktop.prototype.reset = function (callback, skipConfirmDialog) { if (this.pedalboardModified && !skipConfirmDialog && !confirm("There are unsaved modifications that will be lost. Are you sure?")) { return diff --git a/html/js/modgui.js b/html/js/modgui.js index 25a8feac4..bb70b92cc 100644 --- a/html/js/modgui.js +++ b/html/js/modgui.js @@ -57,6 +57,25 @@ function loadFileTypesList(parameter, dummy, callback) { }) } +// Only worth expanding once the list outgrows the box. Re-run this whenever the options +// change: a list that was short enough at build time may not be after a download. +function assignFileListExpand(elem) { + var list = elem.find('.mod-enumerated-list') + var button = elem.find('.file-list-btn-expand').off('click') + + if (list.length == 1 && list[0].childElementCount > 5) { + button.show().click(function () { + if (elem.hasClass('expanded')) { + elem.removeClass('expanded') + } else { + elem.addClass('expanded') + } + }) + } else { + button.hide() + } +} + function loadDependencies(gui, effect, dummy, callback) { //source, effect, bundle, callback) { var iconLoaded = true var settingsLoaded = true @@ -523,6 +542,96 @@ function GUI(effect, options) { self.triggerJS({ type: 'change', symbol: symbol, value: value }) } + /* A file dropdown is filled once, from the /files/list snapshot taken while the GUI was + being built, and nothing ever re-reads it -- so a file added afterwards stays invisible + to a plugin already on the board. The backend scan is always live; the staleness is + entirely here. + + Re-render the plugin's own templates and swap in just the file-list widgets. Going + through the template is what makes this work for any plugin: we never build the option + nodes ourselves, so we never assume how the author nested them. Re-rendering the whole + icon is not an option -- its port elements carry the jsPlumb endpoints the connection + manager holds on to, so replacing them would cut every cable into this plugin. */ + this.refreshFileTypesLists = function (fileType, hoist) { + var parameters = [] + for (var i in effect.parameters) { + var parameter = effect.parameters[i] + if (parameter.path && parameter.fileTypes && parameter.fileTypes.indexOf(fileType) >= 0) { + parameters.push(parameter) + } + } + if (!parameters.length) { + return + } + + /* The list arrives sorted by path. Lift the files named in `hoist` to the front, so a + tone the user just downloaded is the first thing in the dropdown instead of buried + wherever its name happened to sort. Only for as long as this GUI lives -- a reload + takes the list straight from /files/list again, in its own order. */ + var reorder = function (files) { + if (!hoist || !hoist.length) { + return files + } + var isNew = function (file) { + return hoist.indexOf(file.fullname) >= 0 + } + return files.filter(isNew).concat(files.filter(function (file) { return !isNew(file) })) + } + + // The templates read effect.parameters, so refresh those, not the self.parameters copies. + var pending = parameters.length + parameters.forEach(function (parameter) { + loadFileTypesList(parameter, false, function () { + parameter.files = reorder(parameter.files) + if (--pending === 0) { + self.swapFileWidgets() + } + }) + }) + } + + this.swapFileWidgets = function () { + var templateData = self.getTemplateData(effect, self.skipNamespace) + var panels = [ + [self.icon, effect.gui.iconTemplate || options.defaultIconTemplate], + [self.settings, effect.gui.settingsTemplate || options.defaultSettingsTemplate], + ] + + panels.forEach(function (panel) { + var live = panel[0] + if (!live) { + return + } + var fresh = $('
').html(Mustache.render(panel[1], templateData)) + + live.find('[mod-widget=custom-select-path]').each(function () { + var old = $(this) + var uri = old.attr('mod-parameter-uri') + var node = fresh.find('[mod-widget=custom-select-path][mod-parameter-uri="' + uri + '"]') + if (node.length !== 1) { + return + } + + var parameter = self.parameters[uri] + parameter.widgets = parameter.widgets.filter(function (widget) { + return widget[0] !== old[0] + }) + + /* assignControlFunctionality reads mod-instance off the element it is given and + only looks downwards, so hand it a stand-in for the panel holding one widget. + It re-registers the widget and restores the selection from parameter.value. */ + var holder = $('
') + if (self.instance) { + holder.attr('mod-instance', self.instance) + } + self.assignControlFunctionality(holder.append(node), false) + + old.replaceWith(node) + assignFileListExpand(node.closest('.mod-file-list')) + }) + }) + } + // lv2 patch messages, mostly used for parameters this.lv2PatchGet = function (uri) { // let the host know about this @@ -804,6 +913,7 @@ function GUI(effect, options) { this.render = function (instance, callback, skipNamespace) { self.instance = instance + self.skipNamespace = skipNamespace var render = function () { self.preRender() @@ -1020,19 +1130,7 @@ function GUI(effect, options) { if (instance && self.effect.parameters.length) { self.settings.find('.mod-file-list').each(function () { - var elem = $(this) - var list = elem.find('.mod-enumerated-list') - if (list.length == 1 && list[0].childElementCount > 5) { - elem.find('.file-list-btn-expand').click(function () { - if (elem.hasClass('expanded')) { - elem.removeClass('expanded') - } else { - elem.addClass('expanded') - } - }) - } else { - elem.find('.file-list-btn-expand').hide() - } + assignFileListExpand($(this)) }) } diff --git a/html/js/tone3000.js b/html/js/tone3000.js new file mode 100644 index 000000000..7319d043a --- /dev/null +++ b/html/js/tone3000.js @@ -0,0 +1,284 @@ +// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +/* + * Tone3000 Select flow. + * + * This runs in a popup, not an iframe, and that is not cosmetic. TONE3000's sign-in + * cookie is SameSite=Lax, so it is never sent on a cross-site iframe subrequest: the + * OTP verifies, the cookie comes back, the next request drops it, and the user lands + * on the login screen again -- silently, with no console error, in every browser. + * Only TONE3000 can change that (SameSite=None; Secure; Partitioned, or calling + * requestStorageAccess() from their own page). A popup is a top-level browsing + * context, so its cookies are first-party and none of this applies. + * + * This file only opens that popup and mints the PKCE pair it will need. Everything + * that happens after the user picks a tone -- the token exchange, the API reads, the + * download itself -- belongs to tone3000-callback.html, which TONE3000 redirects the + * popup to. That page is served from our origin, so it reads the pair straight out of + * the sessionStorage written below. + */ + +var TONE3000_STATE_KEY = 't3k_state' +var TONE3000_VERIFIER_KEY = 't3k_code_verifier' +var TONE3000_REDIRECT_PATH = '/tone3000-callback.html' +var TONE3000_POPUP_NAME = 't3k_select' + +var tone3000Popup = null + +function tone3000Base64Url (bytes) { + var str = String.fromCharCode.apply(null, new Uint8Array(bytes)) + return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') +} + +function tone3000RandomBase64Url (nbytes) { + return tone3000Base64Url(crypto.getRandomValues(new Uint8Array(nbytes))) +} + +// S256 PKCE challenge. crypto.subtle only exists in a secure context. localhost is +// one; the real device, on plain http:// over a LAN IP, is not. A pure-JS sha256 +// will be needed before this ships to hardware. +function tone3000Sha256Base64Url (input) { + return crypto.subtle.digest('SHA-256', new TextEncoder().encode(input)) + .then(tone3000Base64Url) +} + +/* Build the authorize URL, stashing the PKCE verifier and CSRF state in OUR + sessionStorage. The popup gets only a snapshot of it, taken at window.open() time, + which is before these are written -- but the callback page is same-origin with us + and reads them back live through window.opener. */ +function tone3000BuildAuthorizeUrl () { + var verifier = tone3000RandomBase64Url(32) + var state = tone3000RandomBase64Url(16) + + return tone3000Sha256Base64Url(verifier).then(function (challenge) { + sessionStorage.setItem(TONE3000_VERIFIER_KEY, verifier) + sessionStorage.setItem(TONE3000_STATE_KEY, state) + + var params = { + client_id: TONE3000_CLIENT_ID, + redirect_uri: window.location.origin + TONE3000_REDIRECT_PATH, + response_type: 'code', + code_challenge: challenge, + code_challenge_method: 'S256', + state: state, + prompt: 'select_tone', + // The NAM plugin is what consumes these files; don't offer the user + // formats this device cannot load. + format: 'nam', + } + + var qs = [] + for (var key in params) { + qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])) + } + return TONE3000_API + '/api/v1/oauth/authorize?' + qs.join('&') + }) +} + +// Headroom for the popup's own title bar, plus whatever the desktop reserves at the +// bottom of the screen. Tuned by eye; nothing can measure it before the popup exists. +var TONE3000_POPUP_CHROME = 165 + +// What `outerHeight - innerHeight` misses. Chrome under-reports the height of its own +// toolbars by about this much; the true value cannot be recovered from anything a page +// can read. Unused where mozInnerScreenY exists, which needs no estimate at all. +var TONE3000_CHROME_UNDERREPORT = 35 + +/* Screen y of the top of our viewport. + * + * Firefox says so exactly. Everyone else gets `screenY` (the window frame) plus a guess + * at the chrome above the viewport, and the guess runs short -- see the constant. */ +function tone3000ViewportTop () { + if (window.mozInnerScreenY !== undefined) { + return window.mozInnerScreenY + } + return window.screenY + (window.outerHeight - window.innerHeight) + TONE3000_CHROME_UNDERREPORT +} + +/* Cover the panel's content area, starting below our own title bar rather than on top + * of it. #tone3000-wrapper *is* that area, so measure it instead of guessing -- move + * the bar in CSS and the popup follows. */ +function tone3000PopupBounds () { + var wrapper = $('#tone3000-wrapper')[0] + + return { + left: Math.round(window.screenX), + top: Math.round(tone3000ViewportTop() + wrapper.getBoundingClientRect().top), + width: window.innerWidth, + height: window.innerHeight - TONE3000_POPUP_CHROME, + } +} + +function tone3000PopupFeatures (bounds) { + return 'width=' + bounds.width + + ',height=' + bounds.height + + ',left=' + bounds.left + ',top=' + bounds.top + + ',toolbar=no,menubar=no,location=no,status=no,resizable=yes,scrollbars=yes' +} + +// How long to keep nudging the popup into place, and how often. It is shown within a +// frame or two; the rest is slack. +var TONE3000_PLACE_TRIES = 6 +var TONE3000_PLACE_INTERVAL = 40 + +/* The feature string's left/top are a hint, and a browser drops them when the opener is + * maximized -- width and height survive, position does not. moveTo is the lever left, + * and only here: the popup is still the about:blank we opened, so it is same-origin. + * One navigation later it is tone3000.com and moveTo throws. + * + * A single move does not take: window.open() returns before the browser has shown the + * window, and the bounds it was created with are applied at show time, over the top of + * anything we set first. So keep moving it for a beat. Where the feature string was + * honoured this is all no-ops. */ +function tone3000PlacePopup (popup, bounds) { + return new Promise(function (resolve) { + var attempts = 0 + function attempt () { + attempts += 1 + try { + popup.moveTo(bounds.left, bounds.top) + } catch (e) { + // Blocked (Firefox's dom.disable_window_move_resize) or already navigated. + resolve() + return + } + if (attempts >= TONE3000_PLACE_TRIES || popup.closed) { + resolve() + return + } + window.setTimeout(attempt, TONE3000_PLACE_INTERVAL) + } + attempt() + }) +} + +function tone3000OpenSelectPopup () { + if (!TONE3000_CLIENT_ID) { + alert('TONE3000 is not configured on this device: MOD_TONE3000_CLIENT_ID is not set.') + return + } + + if (tone3000Popup && !tone3000Popup.closed) { + tone3000Popup.focus() + return + } + + // Open synchronously, while we still hold the user activation from the click. + // Deferring window.open until after the PKCE promise resolves gets it blocked + // as an unsolicited popup. + var bounds = tone3000PopupBounds() + tone3000Popup = window.open('', TONE3000_POPUP_NAME, tone3000PopupFeatures(bounds)) + if (!tone3000Popup) { + alert('Popup blocked -- allow popups for this site and try again.') + return + } + + // Both have to finish before the popup leaves about:blank: the URL is what we + // navigate to, and after navigating we can no longer move it. + var popup = tone3000Popup + Promise.all([ + tone3000BuildAuthorizeUrl(), + tone3000PlacePopup(popup, bounds), + ]).then(function (results) { + popup.location = results[0] + }).catch(function (err) { + popup.close() + alert('Failed to build the TONE3000 authorize URL: ' + err) + }) +} + +/* Called from the popup once a download lands -- it is same-origin with us, so it reaches + this directly. Every plugin already on the board is holding a file list from before the + download, so hand each one a fresh copy, with the files just written at the top of it. + `downloaded` holds the paths the upload route reported writing. */ +function tone3000RefreshFileLists (downloaded) { + if (typeof desktop === 'undefined' || !desktop || !desktop.pedalboard) { + return + } + var plugins = desktop.pedalboard.data('plugins') + for (var instance in plugins) { + var plugin = plugins[instance] + var gui = (plugin && plugin.data) ? plugin.data('gui') : null + if (gui && gui.refreshFileTypesLists) { + gui.refreshFileTypesLists('nammodel', downloaded) + } + } +} + +function tone3000ClosePopup () { + if (tone3000Popup && !tone3000Popup.closed) { + tone3000Popup.close() + } + tone3000Popup = null + + sessionStorage.removeItem(TONE3000_STATE_KEY) + sessionStorage.removeItem(TONE3000_VERIFIER_KEY) +} + +/* A popup outlives the page that opened it, and the callback page reads the PKCE pair + out of this window -- so a reload or a navigation would strand it on screen with no + opener left to read. pagehide (not unload) is the event that still fires when the + page goes into the bfcache. */ +window.addEventListener('pagehide', tone3000ClosePopup) + +/* Browser-side for now: this is a preference about this browser's popup, not device state, + and there is no user-settings route to put it behind. localStorage throws outright when + storage is disabled, rather than degrading, so every access is guarded. */ +var TONE3000_AUTO_OPEN_KEY = 't3k_auto_open' + +function tone3000AutoOpen () { + try { + return localStorage.getItem(TONE3000_AUTO_OPEN_KEY) === '1' + } catch (e) { + return false + } +} + +function tone3000SetAutoOpen (enabled) { + try { + localStorage.setItem(TONE3000_AUTO_OPEN_KEY, enabled ? '1' : '0') + } catch (e) { + // Nothing to do -- the checkbox still works for this session. + } +} + +JqueryClass('tone3000Box', { + init: function (options) { + var self = $(this) + + options = $.extend({ + isMainWindow: true, + windowName: "Tone3000", + }, options) + + self.data(options) + + self.find('#tone3000-browse').click(function () { + tone3000OpenSelectPopup() + return false + }) + + var autoOpen = self.find('#tone3000-autoopen') + autoOpen.prop('checked', tone3000AutoOpen()) + autoOpen.change(function () { + tone3000SetAutoOpen(this.checked) + }) + + /* windowopen fires inside the click that selected the tab, so we still hold the user + activation window.open needs. Opening the popup any later gets it blocked. + + There is deliberately no windowclose handler. It fires whenever the panel hides -- + including when the window manager hides it to raise another panel -- and closing + the popup there meant every trip through another tab threw away the user's place + in the catalog. The popup outlives the panel; only leaving the page closes it. */ + options.open = function () { + if (tone3000AutoOpen()) { + tone3000OpenSelectPopup() + } + return false + } + + self.window(options) + }, +}) diff --git a/html/tone3000-callback.html b/html/tone3000-callback.html new file mode 100644 index 000000000..511ef431d --- /dev/null +++ b/html/tone3000-callback.html @@ -0,0 +1,424 @@ + + + + + Tone3000 download + + + + +
+

TONE3000

+

Getting your tone…

+

Exchanging the authorization code…

+ + + + + + +
+ + + + diff --git a/mod/settings.py b/mod/settings.py index 6f9958cbf..2d79368d2 100644 --- a/mod/settings.py +++ b/mod/settings.py @@ -83,6 +83,11 @@ CONTROLCHAIN_HTTP_ADDRESS = os.environ.pop('MOD_CONTROLCHAIN_HTTP_ADDRESS', "https://download.mod.audio/releases/cc-firmware/v3") +# Tone3000 integration. The client id is the OAuth publishable key (t3k_pub_...), +# a public value: it identifies the app, it grants no data access on its own. +TONE3000_CLIENT_ID = os.environ.get('MOD_TONE3000_CLIENT_ID', "") +TONE3000_API = os.environ.get('MOD_TONE3000_API', "https://www.tone3000.com") + MIDI_BEAT_CLOCK_SENDER_URI = "urn:mod:mclk" MIDI_BEAT_CLOCK_SENDER_INSTANCE_ID = 9993 MIDI_BEAT_CLOCK_SENDER_OUTPUT_PORT = "mclk" # This is the LV2 symbol of the plug-ins OutputPort diff --git a/mod/webserver.py b/mod/webserver.py index 808c8532b..f787452ca 100644 --- a/mod/webserver.py +++ b/mod/webserver.py @@ -38,7 +38,8 @@ DEFAULT_ICON_TEMPLATE, DEFAULT_SETTINGS_TEMPLATE, DEFAULT_ICON_IMAGE, DEFAULT_PEDALBOARD, DEFAULT_SNAPSHOT_NAME, DATA_DIR, KEYS_PATH, USER_FILES_DIR, FAVORITES_JSON_FILE, PREFERENCES_JSON_FILE, USER_ID_JSON_FILE, - DEV_HOST, UNTITLED_PEDALBOARD_NAME, MODEL_CPU, MODEL_TYPE, PEDALBOARDS_LABS_HTTP_ADDRESS) + DEV_HOST, UNTITLED_PEDALBOARD_NAME, MODEL_CPU, MODEL_TYPE, PEDALBOARDS_LABS_HTTP_ADDRESS, + TONE3000_CLIENT_ID, TONE3000_API) from mod import ( TextFileFlusher, WINDOWS, @@ -1819,6 +1820,8 @@ def index(self): 'preferences': json.dumps(SESSION.prefs.prefs), 'bufferSize': get_jack_buffer_size(), 'sampleRate': get_jack_sample_rate(), + 'tone3000_client_id': mod_squeeze(TONE3000_CLIENT_ID), + 'tone3000_api': mod_squeeze(TONE3000_API), } return context @@ -2260,6 +2263,38 @@ def get(self): 'files': tuple(retfiles[fn] for fn in fullnames), }) +class FilesUpload(JsonRequestHandler): + def post(self, filetype): + datadir, extensions = FilesList._get_dir_and_extensions_for_filetype(filetype) + if datadir is None: + raise web.HTTPError(400) + + # Anything outside the 3 CORS "simple" content-types forces a preflight we do not answer, + # so a cross-origin page cannot reach this handler. + if self.request.headers.get("Content-Type") != "application/octet-stream": + raise web.HTTPError(400) + + # basename() alone would quietly turn "../../etc" into "etc" and write it anyway. + # Refuse anything it would rewrite, so a caller never gets a file somewhere it did + # not ask for. Both are single path components by the time we join them. + folder = self.get_argument("folder", "") + name = self.get_argument("name", "") + if folder != os.path.basename(folder) or name != os.path.basename(name): + raise web.HTTPError(400) + if folder in ("", ".", "..") or name in ("", ".", ".."): + raise web.HTTPError(400) + if not name.lower().endswith(extensions): + raise web.HTTPError(400) + + destdir = os.path.join(USER_FILES_DIR, datadir, folder) + os.makedirs(destdir, exist_ok=True) + fullname = os.path.join(destdir, name) + with open(fullname, 'wb') as fh: + fh.write(self.request.body) + + # Same string FilesList builds by walking, so a caller can match on it. + self.write({'ok': True, 'fullname': fullname}) + settings = {'log_function': lambda handler: None} if not LOG else {} application = web.Application( @@ -2352,6 +2387,7 @@ def get(self): # file listing etc (r"/files/list/?", FilesList), + (r"/files/upload/([a-z]+)/?", FilesUpload), (r"/reset/?", DashboardClean), diff --git a/package.json b/package.json new file mode 100644 index 000000000..443cc187c --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "mod-ui", + "private": true, + "description": "Frontend tests for mod-ui. The app itself is browser-global JS served from html/; this package exists only to run the test/js/ suite under Node's built-in test runner.", + "scripts": { + "test": "node --test \"test/js/**/*.test.js\"", + "test:watch": "node --test --watch \"test/js/**/*.test.js\"" + }, + "devDependencies": { + "jsdom": "^29.1.1" + } +} diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..3655b1c20 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,36 @@ +[pytest] +testpaths = test +python_files = test_*.py +markers = + lilv: tests that initialize the global lilv world (opt-in; run with -m lilv). + Deliberately excluded from the default run by addopts below -- see + docs/characterization-phase-6.md. These tests call modtools.utils.init(), + which is process-global and has no de-init in this suite (cleanup() + exists but is intentionally not called -- see test/test_effects_lilv.py). + A combined run (`pytest -m "(lilv or not lilv) and not lilv_fixture"` + -- see the lilv_fixture marker below for why the "and not lilv_fixture" + is required) was verified green with no observed leakage into other + tests as of phase 6, but -m lilv should still be treated as its own + invocation in CI to keep the blast radius of a lilv/JACK-adjacent + regression contained to one job. + lilv_fixture: the phase-6 stretch-goal tests (test/test_effects_lilv_fixture.py) + that scan a hand-written, binaryless .lv2 bundle. Opt-in; run with + -m lilv_fixture, and ONLY that -- as its OWN, separate pytest + invocation. DO NOT run together with -m lilv: modtools.utils.init() + is only safe to call ONCE per process. Calling it a second time (this + module's fixture vs. test_effects_lilv.py's) leaves lilv's + process-global NamespaceDefinitions singleton bound to a freed world + -- confirmed by direct reproduction to silently return empty + ports/category data and then SIGSEGV the interpreter on exit. Keeping + this marker's tests in a file of their own, with their own single + init() call and their own `pytest -m lilv_fixture` invocation, is not + a style preference, it is the only way to keep this suite + segfault-free. + DANGER, easy to get wrong: a bare `-m "lilv or not lilv"` (the + phase-6 spec's combined-run check) ALSO collects lilv_fixture tests, + because a lilv_fixture-only test is NOT marked lilv and therefore + satisfies "not lilv" -- that recreates the exact double-init crash + above. When running that combined check, always add + `and not lilv_fixture`: + `pytest -m "(lilv or not lilv) and not lilv_fixture"`. +addopts = -m "not lilv and not lilv_fixture" diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 000000000..6957b218d --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,4 @@ +pytest + +# Python 3.12 removed ssl.match_hostname, which tornado 4.3 falls back to importing from this package +backports.ssl_match_hostname; python_version >= "3.12" diff --git a/test/base.py b/test/base.py new file mode 100644 index 000000000..958dd9a0d --- /dev/null +++ b/test/base.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Shared base class for layer-1 characterization tests. + +Uses tornado 4.3's tornado.testing.AsyncHTTPTestCase (unittest-based, plain +tornado-4 idioms -- no async/await) against the real module-level +mod.webserver.application. conftest.py has already set every MOD_* env var +before this module (or any test module) imports mod.webserver. +""" + +import json + +from tornado.testing import AsyncHTTPTestCase + +from conftest import _UserFilesHelper, _USER_FILES_DIR + + +class ModUITestCase(AsyncHTTPTestCase): + # Every test_*.py imports this class directly (`from base import + # ModUITestCase`) so it can subclass it -- which also puts it in each + # test module's globals. pytest's unittest integration collects *any* + # TestCase subclass it finds at module scope, regardless of name (the + # usual python_classes="Test*" filter does not apply to TestCase + # subclasses), so without this it would try to collect ModUITestCase + # itself as a test in every module that imports it and fail with + # "no attribute 'runTest'" (it defines no test_* methods of its own). + # Concrete subclasses below must set __test__ = True to opt back in, + # since __test__ is looked up via normal attribute inheritance. + __test__ = False + + def runTest(self): + # Never actually invoked as a real test (real test_* methods are + # collected and run individually by pytest). Exists only because + # modern pytest (9.x) instantiates every unittest.TestCase subclass + # once with methodName="runTest" during collection, to register + # fixture factories (_pytest.unittest.UnitTestCase.newinstance()). + # tornado 4.3's own AsyncTestCase.__init__ (tornado/testing.py) + # unconditionally does getattr(self, methodName) -- unlike stdlib + # unittest.TestCase, it does not special-case a missing "runTest" -- + # so without this method that probe instantiation raises + # AttributeError and collection fails for every test class. + pass + + def get_app(self): + # Imported lazily (not at module top-level) so that conftest.py's + # env-var setup is guaranteed to have already run before mod.webserver + # -- and mod.settings, which reads MOD_* env vars at import time -- + # is ever imported. + import mod.webserver + return mod.webserver.application + + def seed(self, relpath, content=b"data"): + """Write USER_FILES_DIR/ (creating parent dirs) for a test. + + Thin wrapper around conftest's user_files fixture helper, exposed + here because pytest fixture return values can't be injected as + parameters into unittest.TestCase test methods (which is what these + AsyncHTTPTestCase-derived tests are). The autouse `user_files` + fixture in conftest.py still handles wiping USER_FILES_DIR between + tests. + """ + return _UserFilesHelper(_USER_FILES_DIR).seed(relpath, content=content) + + def fetch_json(self, path, **kwargs): + """GET/POST/etc. `path` and decode the response body as JSON. + + Asserts the response declares a JSON content type, matching the + JsonRequestHandler.write() convention used across mod/webserver.py. + """ + response = self.fetch(path, **kwargs) + content_type = response.headers.get("Content-Type", "") + assert "application/json" in content_type, ( + "expected JSON content type for %s, got %r (body: %r)" + % (path, content_type, response.body) + ) + return response, json.loads(response.body.decode("utf-8")) diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 000000000..a480d3325 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +""" +Layer-1 characterization test harness bootstrap. + +This module runs at collection time, BEFORE any test module gets a chance to +``import mod.webserver`` (and transitively ``mod.settings``, which reads every +``MOD_*`` env var at import time). So all environment setup below is plain +module-level code, not a fixture -- fixtures would run too late. + +These tests exercise the real ``mod.webserver.application`` (the module-level +tornado ``web.Application`` built at import time) against a throwaway on-disk +tree. Nothing here calls ``mod.webserver.run()`` or ``prepare()`` -- those pull +in real hardware / mod-host / JACK setup that isn't available in this dev +environment. + +NEVER-CALL LIST -- routes no test in this suite may fetch. They run +subprocesses, write outside the sandboxed temp tree, or block forever: + +- ``/system/cleanup`` deletes ``~/.pedalboards`` and ``~/.lv2`` +- ``/system/exechange`` reboot/systemctl/mod-backup subprocesses +- ``/update/download/``, ``/update/begin`` writes under /data, /tmp +- ``/controlchain/download/`` firmware download, subprocess mv to /tmp +- ``/switch_cpu_freq/`` writes /sys/devices/system/cpu +- ``/recording/*`` needs JACK; ``play/wait`` long-polls forever +- ``/pedalboard/pack_bundle``, ``/pedalboard/load_web``, + ``/pedalboard/factorycopy``, ``/pedalboard/image/generate`` subprocesses +- ``/effect/install``, ``/sdk/install``, ``/package/uninstall`` touch the + plugin dir via subprocess tar / rmtree +- ``/effect/list``, ``/effect/get*``, ``/effect/bulk``, ``/effect/add``, + ``/effect/image``, ``/effect/file``, ``/resources/(.*)?uri=`` dereference + the global lilv world, which is NULL until ``modtools.utils.init()`` runs + -- calling them without init SEGFAULTS the test process (see the phase-6 + spec in docs/ before touching these) +- ``/pedalboard/list``, ``/banks/`` while a real pedalboard bundle exists in + the sandbox pedalboards dir -- get_all_pedalboards SEGFAULTS parsing it + (NamespaceDefinitions::init / lilv_new_uri, needs the uninitialized global + lilv world). Both are safe against an EMPTY pedalboards dir; save/remove a + bundle within one test and only list after the dir is empty again +- ``/effect/remove/`` HANGS (no crash, no timeout server-side -- + the response is simply never sent) for any instance name not already + registered in ``SESSION.host.mapper``, which is every instance name in + this sandbox (``POST /effect/add`` is itself banned above, so no instance + can ever be registered). Root cause: ``Host.remove_plugin`` + (``mod/host.py:2604``, ``@gen.coroutine``) calls + ``self.mapper.get_id_without_creating(instance)`` *before* its own + try/except KeyError guard around ``self.plugins.pop(...)`` a few lines + down -- the KeyError from the lookup itself is swallowed into the + coroutine's Future instead of propagating, so the handler's + ``callback(False)`` is never reached and ``gen.Task`` in ``EffectRemove`` + never resolves. Confirmed by probing with a 4s client-side + ``request_timeout``: HTTP 599 (client timeout), not a fast response. See + ``test/test_host_commands.py`` module docstring (phase 4). +""" + +import atexit +import os +import shutil +import sys +import tempfile + +import pytest + +# ---------------------------------------------------------------------------- +# 0. Fail fast if the native extension mod.webserver depends on isn't built. +# ---------------------------------------------------------------------------- + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +_LIBMOD_UTILS_SO = os.path.join(REPO_ROOT, "utils", "libmod_utils.so") + +if not os.path.isfile(_LIBMOD_UTILS_SO): + pytest.exit( + "libmod_utils.so not found at {0}.\n" + "Build it first: `make -C utils` (from the repo root). See " + "mod-ui/CLAUDE.md for one-time environment setup.".format(_LIBMOD_UTILS_SO), + returncode=1, + ) + +# ---------------------------------------------------------------------------- +# 1. Build a throwaway data/user-files tree and point every MOD_* env var +# that mod.settings reads at it, BEFORE mod.webserver (and mod.settings) +# is ever imported by a test module. +# ---------------------------------------------------------------------------- + +_TEST_ROOT = tempfile.mkdtemp(prefix="modui-test-") + +_DATA_DIR = os.path.join(_TEST_ROOT, "data") +_USER_FILES_DIR = os.path.join(_TEST_ROOT, "user-files") +_PEDALBOARDS_DIR = os.path.join(_TEST_ROOT, "pedalboards") +_PLUGINS_DIR = os.path.join(_TEST_ROOT, "lv2") +# Empty, dedicated -- deliberately NOT the same dir as _PLUGINS_DIR +# (MOD_USER_PLUGINS_DIR) above, to avoid conflating "where mod-ui thinks user +# plugins live" with "what lilv scans for the phase-6 tests" even though +# both happen to be empty today. +_LV2_SCAN_DIR = os.path.join(_TEST_ROOT, "lv2-path-empty") + +os.makedirs(_DATA_DIR, exist_ok=True) +os.makedirs(_USER_FILES_DIR, exist_ok=True) +os.makedirs(_PEDALBOARDS_DIR, exist_ok=True) +os.makedirs(_PLUGINS_DIR, exist_ok=True) +os.makedirs(_LV2_SCAN_DIR, exist_ok=True) + +# Replicate the bits of mod.check_environment() that handlers rely on but +# that we never call (check_environment() only runs inside prepare()). +with open(os.path.join(_DATA_DIR, "banks.json"), "w") as fh: + fh.write("[]") +with open(os.path.join(_DATA_DIR, "favorites.json"), "w") as fh: + fh.write("[]") + +os.environ["MOD_DEV_ENVIRONMENT"] = "1" +os.environ["MOD_LOG"] = "0" +os.environ["MOD_DATA_DIR"] = _DATA_DIR +os.environ["MOD_USER_FILES_DIR"] = _USER_FILES_DIR +os.environ["MOD_HTML_DIR"] = os.path.join(REPO_ROOT, "html") +os.environ["MOD_DEFAULT_PEDALBOARD"] = os.path.join(REPO_ROOT, "default.pedalboard") +# Without these two, mod.settings defaults LV2_PEDALBOARDS_DIR/LV2_PLUGIN_DIR +# to ~/.pedalboards and ~/.lv2 -- and pedalboard save/remove handlers would +# write into the REAL user home instead of the sandbox. +os.environ["MOD_USER_PEDALBOARDS_DIR"] = _PEDALBOARDS_DIR +os.environ["MOD_USER_PLUGINS_DIR"] = _PLUGINS_DIR + +# Phase 6 (docs/characterization-phase-6.md): lilv (the C++ library backing +# modtools.utils.init()) reads the LV2_PATH env var itself, via plain getenv, +# at lilv_world_load_all() time inside init() -- not at Python import time +# and not cached anywhere in Python. Confirmed by reading utils/utils_lilv.cpp +# init() (~:3893): it only calls lilv_world_new()/lilv_world_load_all(W), with +# no explicit LV2_PATH manipulation of its own (unlike e.g. get_all_pedalboards +# a few hundred lines below, which saves/restores LV2_PATH around its own +# private world). So setting os.environ["LV2_PATH"] here, well before any +# phase-6 test module even calls init() at fixture time, is sufficient -- +# CPython's os.environ.__setitem__ calls os.putenv() under the hood, which is +# what a later getenv() in the C library will see. Point it at an empty, +# sandboxed dir so the lilv world phase 6 builds is always empty and +# deterministic, never the machine's real ~/.lv2 or /usr/lib/lv2. +os.environ["LV2_PATH"] = _LV2_SCAN_DIR + +# ---------------------------------------------------------------------------- +# 1b. Sandbox guard: every writable path mod.settings resolves must live +# under the throwaway test root. mod.settings only reads env vars and +# stdlib (importing it here is cheap and does NOT load libmod_utils.so), +# so this catches a broken/renamed MOD_* env var before any test can +# write outside the sandbox. +# ---------------------------------------------------------------------------- + +from mod import settings as _mod_settings # noqa: E402 (needs env set above) + +for _name in ("DATA_DIR", "USER_FILES_DIR", "LV2_PEDALBOARDS_DIR", "LV2_PLUGIN_DIR"): + _path = os.path.realpath(getattr(_mod_settings, _name)) + if not _path.startswith(os.path.realpath(_TEST_ROOT) + os.sep): + pytest.exit( + "SANDBOX GUARD: mod.settings.{0} resolves to {1}, which is outside " + "the test root {2}. Refusing to run -- tests could write into real " + "user/system directories. Check the MOD_* env vars set in " + "test/conftest.py against mod/settings.py.".format(_name, _path, _TEST_ROOT), + returncode=1, + ) + +# Make the repo importable the same way server.py does (test/ is not on +# sys.path by default under some pytest invocations). +if REPO_ROOT not in sys.path: + sys.path.insert(0, REPO_ROOT) + + +def _cleanup_test_root(): + shutil.rmtree(_TEST_ROOT, ignore_errors=True) + + +atexit.register(_cleanup_test_root) + +# ---------------------------------------------------------------------------- +# 2. Fixtures +# ---------------------------------------------------------------------------- + + +class _UserFilesHelper(object): + """Exposes USER_FILES_DIR plus a seed() helper to test modules.""" + + def __init__(self, root): + self.root = root + + def seed(self, relpath, content=b"data"): + """Create USER_FILES_DIR/ (and any parent dirs) with content.""" + fullpath = os.path.join(self.root, relpath) + os.makedirs(os.path.dirname(fullpath), exist_ok=True) + mode = "wb" if isinstance(content, bytes) else "w" + with open(fullpath, mode) as fh: + fh.write(content) + return fullpath + + +def _reset_user_files_dir(): + if os.path.isdir(_USER_FILES_DIR): + shutil.rmtree(_USER_FILES_DIR) + os.makedirs(_USER_FILES_DIR, exist_ok=True) + + +@pytest.fixture(autouse=True) +def user_files(): + """Function-scoped, autouse: wipe and recreate USER_FILES_DIR around + every test, so tests never see leftovers from one another. + + USER_FILES_DIR itself is fixed for the whole process (mod.settings reads + MOD_USER_FILES_DIR once, at import time), so we can't swap directories + per-test -- instead we clear its contents. + + autouse=True (rather than requiring tests to depend on it explicitly) + because our test classes are unittest.TestCase subclasses + (tornado.testing.AsyncHTTPTestCase) -- pytest applies autouse fixtures' + setup/teardown around unittest-style tests too, but does NOT support + injecting a fixture's return value as a test-method parameter for them. + Tests that need to seed files use ModUITestCase.seed() (test/base.py) + instead, which points at this same directory. + """ + _reset_user_files_dir() + yield _UserFilesHelper(_USER_FILES_DIR) + _reset_user_files_dir() + + +def _reset_pedalboards_dir(): + if os.path.isdir(_PEDALBOARDS_DIR): + shutil.rmtree(_PEDALBOARDS_DIR) + os.makedirs(_PEDALBOARDS_DIR, exist_ok=True) + + +@pytest.fixture(autouse=True) +def pedalboards_dir(): + """Function-scoped, autouse: wipe/recreate LV2_PEDALBOARDS_DIR and reset + the process-global SESSION around every test. + + Mirrors ``user_files`` above, for the same reason: LV2_PEDALBOARDS_DIR is + fixed for the whole process (mod.settings reads MOD_USER_PEDALBOARDS_DIR + once, at import time), so we clear its contents instead of swapping + directories. Phase 3 (docs/characterization-phase-3.md) is the first + phase whose tests write real pedalboard bundles to disk via + ``SESSION.host.save()`` (through ``POST /pedalboard/save``) and mutate + process-global state on ``SESSION.host`` (``pedalboard_path``, + ``pedalboard_snapshots``, ``current_pedalboard_snapshot_id``, ...) via + ``/pedalboard/load_bundle/`` and ``/snapshot/*``. Neither the on-disk + bundles nor that in-memory state may leak between tests or modules. + + ``SESSION.reset(callback)`` (mod/session.py) runs its callback + synchronously here: under ``MOD_DEV_ENVIRONMENT=1`` the FakeHMI is never + "initialized" (see module docstring), so ``Session.reset`` takes its + synchronous branch straight to ``Host.reset``, and ``FakeHost`` (see + ``mod/development.py``) invokes every ``send_notmodified``/ + ``send_modified`` callback immediately with ``True`` -- no real + mod-host, no IOLoop pump required. + """ + _reset_pedalboards_dir() + from mod.webserver import SESSION + SESSION.reset(lambda ok: None) + yield + SESSION.reset(lambda ok: None) + _reset_pedalboards_dir() diff --git a/test/fixtures/phase6-fixture.lv2/manifest.ttl b/test/fixtures/phase6-fixture.lv2/manifest.ttl new file mode 100644 index 000000000..636a5adb3 --- /dev/null +++ b/test/fixtures/phase6-fixture.lv2/manifest.ttl @@ -0,0 +1,7 @@ +@prefix lv2: . +@prefix rdfs: . + + + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . diff --git a/test/fixtures/phase6-fixture.lv2/phase6-fixture.ttl b/test/fixtures/phase6-fixture.lv2/phase6-fixture.ttl new file mode 100644 index 000000000..8c8561d25 --- /dev/null +++ b/test/fixtures/phase6-fixture.lv2/phase6-fixture.ttl @@ -0,0 +1,25 @@ +@prefix lv2: . +@prefix doap: . +@prefix rdfs: . +@prefix foaf: . + + + a lv2:Plugin, lv2:UtilityPlugin ; + doap:name "Phase 6 Fixture" ; + doap:license ; + doap:maintainer [ + foaf:name "mod-ui characterization tests" + ] ; + lv2:microVersion 0 ; + lv2:minorVersion 1 ; + lv2:port [ + a lv2:InputPort, lv2:AudioPort ; + lv2:index 0 ; + lv2:symbol "in" ; + lv2:name "In" + ] , [ + a lv2:OutputPort, lv2:AudioPort ; + lv2:index 1 ; + lv2:symbol "out" ; + lv2:name "Out" + ] . diff --git a/test/js/README.md b/test/js/README.md new file mode 100644 index 000000000..d0080f3ec --- /dev/null +++ b/test/js/README.md @@ -0,0 +1,65 @@ +# Frontend tests + +Tests for the browser JavaScript in `html/js/`. The Python backend has its own suite in +`test/` (pytest); this is the JS side. + +## Running + +```bash +npm install # once, pulls in jsdom (the only dependency) +npm test # runs everything under test/js/ +npm run test:watch +``` + +Node's built-in test runner (`node --test`, `node:assert`) — no Jest, no Vitest, no build +step. The only third-party dependency is `jsdom`. + +## How it works + +`html/js/*.js` is not modular. It defines globals and assumes `jQuery`, `Mustache` and a few +page variables already exist, so it cannot be `require`d. Instead `harness.js` stands up a +jsdom window, loads the real vendored libraries, then loads the **real source file under +test** with `window.eval` and drives its now-global functions. The tests exercise the +shipped code, not a copy of it. + +`harness.js` exports: + +- `makeWindow({url, body})` — a jsdom window with jQuery/Mustache loaded and the page globals + (`VERSION`, `baseUrl`, `desktop`, `isSDK`) defined. Returns `{window, $, load}` where + `load(rel)` evals a source file, `rel` being relative to `html/`. +- `captureWindowOptions(ctx)` — grabs the options a `JqueryClass` box hands to + `self.window(...)`, without pulling in the real overlay machinery. +- `stubAjax(ctx, routes)` — an `$.ajax` that answers by URL substring. + +## What this can and cannot test + +jsdom has a DOM but no browser around it. So: + +**In scope** — DOM manipulation and logic. Widget rendering and re-rendering, event wiring, +list ordering, preference storage, the shape of data handed between functions. This is where +the bugs these tests were written for actually lived. + +**Out of scope, stays manual** — anything needing a real browser: + +- **Layout.** `getBoundingClientRect` is all zeros in jsdom, so popup *placement* (the + `mozInnerScreenY` / chrome-height math in `tone3000.js`) cannot be tested here. +- **Windowing.** `window.open` is a stub; there is no real popup, no cross-window focus, no + cookie behaviour. +- **CSS.** No cascade, no computed styles. +- **The network and the Tornado backend.** Backend routes are tested in `test/` (pytest); + the `POST /files/upload` guards and the `fullname` round-trip belong there, not here. + +Keep this boundary honest — a green suite that quietly doesn't cover placement or the backend +is worse than no suite, because it reads as coverage. + +## Files + +- `harness.js` — the rig described above (not itself a test; `*.test.js` are the tests). +- `modgui.filelist.test.js` — `GUI.refreshFileTypesLists` / `swapFileWidgets`: refreshing a + plugin's file dropdown in place, across two templates that nest options differently. Runs + against `fixtures/icon-nested.html` and the shipped `html/resources/settings.html`. +- `fixtures/icon-nested.html` — a plugin-icon template whose file options nest inside a + `.mod-enumerated-list`, the way NAM's icon does. Vendored so the suite needs only the + `mod-ui` checkout, not the sibling `mod-fs/` dev tree. +- `tone3000.autoopen.test.js` — the Tone3000 tab's auto-open preference and the popup's + lifetime relative to the tab. diff --git a/test/js/fixtures/icon-nested.html b/test/js/fixtures/icon-nested.html new file mode 100644 index 000000000..589453cbf --- /dev/null +++ b/test/js/fixtures/icon-nested.html @@ -0,0 +1,55 @@ + +
+
+
+ {{#controls}} +
+
{{name}}
+
+
+ {{/controls}} +
+
+ +
+
+ {{#effect.parameters}} + {{#path}} +
+
+ -- choose a model -- +
+
+ {{#files}} +
{{basename}}
+ {{/files}} +
+
+ {{/path}} + {{/effect.parameters}} +
+
+
+ {{#effect.ports.audio.input}} +
+
+
+ {{/effect.ports.audio.input}} +
+
+ {{#effect.ports.audio.output}} +
+
+
+ {{/effect.ports.audio.output}} +
+
diff --git a/test/js/harness.js b/test/js/harness.js new file mode 100644 index 000000000..34bef67f1 --- /dev/null +++ b/test/js/harness.js @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +/* + * Test rig for the browser-global frontend. + * + * html/js/*.js is not modular -- it defines globals and assumes jQuery, Mustache and a few + * page-scoped variables already exist. So it cannot be require()'d. Instead we stand up a + * jsdom window, load the real vendored libraries into it, then load the real source file + * under test with window.eval, and drive its now-global functions. + * + * What this reaches, it tests for real. What jsdom lacks, it cannot: there is no layout + * (getBoundingClientRect is all zeros), no windowing (window.open is a stub), no network, + * no CSS. Popup *placement*, cross-window behaviour and styling are out of scope here and + * stay manual -- see README.md. + */ + +const fs = require('fs') +const path = require('path') +const { JSDOM } = require('jsdom') + +const HTML = path.resolve(__dirname, '../../html') + +// Vendored libraries the frontend takes for granted, in load order. +const LIBS = [ + 'js/lib/jquery-1.9.1.min.js', + 'js/lib/mustache.js', + 'js/lib/sprintf-0.6.js', +] + +/* + * Build a fresh window with jQuery + Mustache loaded and the globals the frontend expects + * to find on the page. Returns { window, $, load } where load(relPath) evals a real source + * file (path relative to html/) into the window. + * + * opts.url -- document origin, e.g. 'http://localhost:8888/' (default about:blank) + * opts.body -- initial innerHTML + * opts.jqueryUI -- stub draggable/droppable/resizable (icons call these); default true + */ +function makeWindow(opts) { + opts = opts || {} + const dom = new JSDOM('' + (opts.body || '') + '', { + runScripts: 'outside-only', + url: opts.url || 'about:blank', + }) + const window = dom.window + const load = rel => window.eval(fs.readFileSync(path.join(HTML, rel), 'utf8')) + + LIBS.forEach(load) + const $ = window.jQuery + + // Page-scoped globals index.html defines before any js/ file runs. modgui.js reads + // VERSION/baseUrl at GUI-construction time; several files reference desktop/isSDK. + window.eval('var desktop = null; var isSDK = false; var VERSION = 1; var baseUrl = "";') + + // jsdom has no TextEncoder on the window; PKCE code paths want it. + if (!window.TextEncoder) { + window.TextEncoder = require('util').TextEncoder + } + + if (opts.jqueryUI !== false) { + ['draggable', 'droppable', 'resizable'].forEach(m => { $.fn[m] = function () { return this } }) + } + + return { window, $, load } +} + +/* + * Capture what a JqueryClass box hands to self.window(options), without pulling in the real + * window.js overlay machinery. Call before loading a file that defines a *Box widget; the + * returned getter yields the latest options object the box registered. + */ +function captureWindowOptions(ctx) { + let captured = null + ctx.window.JqueryClass = function (name, methods) { + ctx.$.fn[name] = function (o) { return methods.init.apply(this, [o]) } + } + ctx.$.fn.window = function (options) { captured = options; return this } + return () => captured +} + +/* + * An $.ajax stub that answers by URL substring. routes is { substring: responseOrFn }; + * a function receives the ajax options and returns the response passed to success. + * Unmatched URLs invoke opts.error if present. Returns a thenable-ish object like $.ajax. + */ +function stubAjax(ctx, routes) { + ctx.$.ajax = opts => { + const url = opts.url || '' + for (const key in routes) { + if (url.indexOf(key) >= 0) { + const r = routes[key] + opts.success(typeof r === 'function' ? r(opts) : r) + return { done: () => {} } + } + } + if (opts.error) opts.error() + return { done: () => {} } + } +} + +module.exports = { makeWindow, captureWindowOptions, stubAjax, HTML } diff --git a/test/js/modgui.filelist.test.js b/test/js/modgui.filelist.test.js new file mode 100644 index 000000000..327453689 --- /dev/null +++ b/test/js/modgui.filelist.test.js @@ -0,0 +1,212 @@ +// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +/* + * GUI.refreshFileTypesLists / swapFileWidgets -- refreshing a plugin's file dropdown in + * place after a download, without rebuilding the icon (which would cut its jsPlumb cables). + * + * Driven against the REAL html/js/modgui.js, rendered over two templates that nest the + * option nodes differently: html/resources/settings.html (options are direct children of + * the widget) and fixtures/icon-nested.html (options nested inside .mod-enumerated-list, + * the shape NAM's icon uses). The point of the swap is to work for both without assuming + * either. + */ + +const { test, before } = require('node:test') +const assert = require('node:assert') +const fs = require('fs') +const path = require('path') +const { makeWindow, stubAjax, HTML } = require('./harness') + +// The icon template nests the file options inside a .mod-enumerated-list; settings.html +// makes them direct children of the widget. Testing both is the point: the swap must not +// assume either shape. The icon fixture is vendored here (see fixtures/icon-nested.html for +// why); settings.html is the real shipped template, read from the checkout. +const ICON = path.resolve(__dirname, 'fixtures/icon-nested.html') + +const URI = 'http://x#model' + +let ctx, $, iconTemplate, settingsTemplate + +before(() => { + ctx = makeWindow({ url: 'http://localhost:8888/' }) + $ = ctx.$ + ctx.load('js/modgui.js') + iconTemplate = fs.readFileSync(ICON, 'utf8') + settingsTemplate = fs.readFileSync(path.join(HTML, 'resources/settings.html'), 'utf8') +}) + +// Fresh file listing the ajax stub serves; each test sets it. +let listing = [] + +function mkEffect() { + return { + uri: 'http://nam', label: 'NAM', name: 'NAM', renderedVersion: 1, + ports: { audio: { input: [], output: [] }, midi: { input: [], output: [] }, + cv: { input: [], output: [] }, control: { input: [], output: [] } }, + presets: [], parameters: [{ + uri: URI, label: 'Model', type: 'http://lv2plug.in/ns/ext/atom#Path', + fileTypes: ['nam', 'nammodel'], ranges: { default: '' }, value: '', + properties: [], units: {}, writable: true, readable: true, comment: '', + shortName: 'Model', symbol: 'model', + }], + gui: { iconTemplate, settingsTemplate, templateData: {}, javascript: null }, + } +} + +// Build a rendered GUI with an initially empty file list, the way loadDependencies would. +function build() { + // loadDependencies re-fetches the templates over ajax even when effect.gui has them, + // so the stub must answer per-URL, not hand everyone the file list. + stubAjax(ctx, { + '/files/list': () => ({ files: listing.slice() }), + 'iconTemplate': iconTemplate, + 'settingsTemplate': settingsTemplate, + }) + const effect = mkEffect() + const gui = new ctx.window.GUI(effect, { + defaultIconTemplate: '
', defaultSettingsTemplate: '
', + change() {}, patchGet() {}, patchSet() {}, presetLoad() {}, + }) + gui.dependenciesLoaded = true + effect.parameters[0].files = [] + effect.parameters[0].path = true + let icon, settings + gui.render('graph/nam_1', (i, s) => { icon = i; settings = s }, false) + return { gui, icon, settings } +} + +const options = root => root.find('[mod-role=enumeration-option]') +const values = root => options(root).map((i, e) => $(e).attr('mod-parameter-value')).get() +const file = (full, base) => ({ fullname: full, basename: base, filetype: 'nammodel' }) + +test('starts with no options in either panel', () => { + listing = [] + const { icon, settings } = build() + assert.equal(options(icon).length, 0) + assert.equal(options(settings).length, 0) +}) + +test('a download lands options inside each template\'s own container', () => { + listing = [] + const { gui, icon, settings } = build() + listing = [ + file('/u/NAM Models/T (1)/T - standard.nam', 'T - standard.nam'), + file('/u/NAM Models/T (1)/T - lite.nam', 'T - lite.nam'), + ] + gui.refreshFileTypesLists('nammodel') + + const iconWidget = icon.find('[mod-widget=custom-select-path]') + assert.equal(iconWidget.length, 1, 'icon widget still present') + assert.equal(options(icon).length, 2, 'icon has both options') + // The icon nests options one level down; they must be inside the list, not floating. + assert.equal(iconWidget.find('.mod-enumerated-list [mod-role=enumeration-option]').length, 2) + assert.equal(iconWidget.children('[mod-role=enumeration-option]').length, 0) + assert.equal(iconWidget.find('[mod-role=input-parameter-value]').length, 1, 'kept value node') + + const setWidget = settings.find('[mod-widget=custom-select-path]') + assert.equal(options(settings).length, 2, 'settings has both options') + // settings.html makes options direct children of the widget. + assert.equal(setWidget.children('[mod-role=enumeration-option]').length, 2) +}) + +test('parameter.widgets holds exactly the live nodes, no stale entries', () => { + listing = [] + const { gui, icon, settings } = build() + listing = [file('/u/NAM Models/T (1)/T - a.nam', 'T - a.nam')] + gui.refreshFileTypesLists('nammodel') + + const param = gui.parameters[URI] + assert.equal(param.widgets.length, 2, 'one per panel') + assert.ok(param.widgets.every(w => + $.contains(icon[0], w[0]) || $.contains(settings[0], w[0])), 'all in the document') + + // A second refresh must not accumulate widgets. + gui.refreshFileTypesLists('nammodel') + assert.equal(gui.parameters[URI].widgets.length, 2) +}) + +test('the current selection survives a refresh, in both panels', () => { + listing = [ + file('/u/NAM Models/T (1)/T - lite.nam', 'T - lite.nam'), + file('/u/NAM Models/T (1)/T - standard.nam', 'T - standard.nam'), + ] + const { gui, icon, settings } = build() + gui.refreshFileTypesLists('nammodel') + const param = gui.parameters[URI] + param.value = '/u/NAM Models/T (1)/T - lite.nam' + gui.setWritableParameterValue(URI, 'p', param.value, null, true) + + listing.push(file('/u/NAM Models/T (2)/T - nano.nam', 'T - nano.nam')) + gui.refreshFileTypesLists('nammodel') + + assert.equal(options(icon).length, 3) + assert.equal(icon.find('.selected').attr('mod-parameter-value'), param.value) + assert.equal(settings.find('.selected').attr('mod-parameter-value'), param.value) +}) + +test('a value that no longer exists selects nothing, like a fresh build', () => { + listing = [file('/u/a.nam', 'a.nam'), file('/u/b.nam', 'b.nam')] + const { gui, icon } = build() + gui.refreshFileTypesLists('nammodel', ['/u/gone.nam']) + // hoisting a missing file is a no-op; selection was never set, so nothing is selected + gui.parameters[URI].value = '/u/gone.nam' + gui.setWritableParameterValue(URI, 'p', '/u/gone.nam', null, true) + assert.equal(icon.find('.selected').length, 0) +}) + +test('clicking a freshly-swapped option fires exactly one patch_set', () => { + listing = [file('/u/a.nam', 'a.nam'), file('/u/b.nam', 'b.nam')] + const { gui, icon } = build() + gui.refreshFileTypesLists('nammodel') + const sets = [] + gui.lv2PatchSet = (uri, vt, v) => sets.push(v) + icon.find('[mod-parameter-value="/u/b.nam"]').click() + assert.deepEqual(sets, ['/u/b.nam']) +}) + +test('hoisted files lead the list, in both panels', () => { + const LITE = '/u/NAM Models/T (1)/T - lite.nam' + const NANO = '/u/NAM Models/T (2)/T - nano.nam' + listing = [ + file(LITE, 'T - lite.nam'), + file('/u/NAM Models/T (1)/T - standard.nam', 'T - standard.nam'), + file(NANO, 'T - nano.nam'), + ] + const { gui, icon, settings } = build() + gui.refreshFileTypesLists('nammodel', [NANO]) + + assert.equal(values(icon)[0], NANO, 'hoisted file first in icon') + assert.equal(values(settings)[0], NANO, 'hoisted file first in settings') + assert.equal(values(icon).length, 3, 'no options lost') + // non-hoisted keep their /files/list (alphabetical) order after the hoisted ones + assert.equal(values(icon)[1], LITE) + assert.ok(values(icon)[2].endsWith('standard.nam')) +}) + +test('two hoisted files both lead, keeping /files/list order among themselves', () => { + const LITE = '/u/NAM Models/T (1)/T - lite.nam' + const NANO = '/u/NAM Models/T (2)/T - nano.nam' + listing = [ + file(LITE, 'T - lite.nam'), + file('/u/NAM Models/T (1)/T - standard.nam', 'T - standard.nam'), + file(NANO, 'T - nano.nam'), + ] + const { gui, icon } = build() + gui.refreshFileTypesLists('nammodel', [NANO, LITE]) + const lead = values(icon).slice(0, 2) + // passed [NANO, LITE] but the list order is lite-before-nano, and that is what's kept + assert.deepEqual(lead, [LITE, NANO]) + assert.ok(values(icon)[2].endsWith('standard.nam')) +}) + +test('without a hoist argument the list keeps /files/list order', () => { + const LITE = '/u/NAM Models/T (1)/T - lite.nam' + listing = [ + file(LITE, 'T - lite.nam'), + file('/u/NAM Models/T (2)/T - nano.nam', 'T - nano.nam'), + ] + const { gui, icon } = build() + gui.refreshFileTypesLists('nammodel') + assert.equal(values(icon)[0], LITE) +}) diff --git a/test/js/tone3000.autoopen.test.js b/test/js/tone3000.autoopen.test.js new file mode 100644 index 000000000..49f1ecb39 --- /dev/null +++ b/test/js/tone3000.autoopen.test.js @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +/* + * tone3000Box -- the auto-open preference and the popup's lifetime relative to the tab. + * + * Driven against the REAL html/js/tone3000.js. The popup is a stub (jsdom has no real + * window.open), so this covers the wiring and the preference, not placement. + * + * Two behaviours pinned here, both of which were bugs at some point: + * - the "open automatically" checkbox persists in localStorage and gates windowopen; + * - selecting the tab while the popup is open FOCUSES it, never re-opens or re-navigates + * it -- there is deliberately no windowclose handler, because windowclose fires whenever + * the panel hides, including when another tab is raised. + */ + +const { test, beforeEach } = require('node:test') +const assert = require('node:assert') +const { makeWindow, captureWindowOptions } = require('./harness') + +const KEY = 't3k_auto_open' +const BODY = '
' + + '
' + + '' + + '' + + '
' + +let ctx, $, getOptions +let opened, popups + +// A stand-in popup. `location` is what a re-navigation would write; `focused` counts focus(). +function makePopup() { + const p = { closed: false, focused: 0, location: '', + focus() { this.focused++ }, close() { this.closed = true } } + popups.push(p) + return p +} + +beforeEach(() => { + ctx = makeWindow({ url: 'http://localhost:8888/', body: BODY }) + $ = ctx.$ + getOptions = captureWindowOptions(ctx) + + opened = 0 + popups = [] + ctx.window.open = () => { opened++; return makePopup() } + ctx.window.alert = () => {} + ctx.window.TONE3000_CLIENT_ID = 'pk_test' + ctx.window.TONE3000_API = 'https://www.tone3000.com' + // crypto.subtle.digest never resolves here, so the authorize URL is never built and the + // popup is never navigated -- exactly what lets us assert location stays ''. window.crypto + // is a read-only accessor in jsdom, so define over it rather than assign. + Object.defineProperty(ctx.window, 'crypto', { + value: { getRandomValues: a => a, subtle: { digest: () => new Promise(() => {}) } }, + configurable: true, + }) + ctx.window.btoa = s => Buffer.from(s, 'binary').toString('base64') + + ctx.load('js/tone3000.js') +}) + +const checkbox = () => $('#tone3000-autoopen') +const initBox = () => $('#t3k').tone3000Box({}) +// The popup persists in module state across initBox(); close it so the next +// "does open() open one?" check starts clean, the way a fresh session would. +const closePopups = () => { popups.forEach(p => { p.closed = true }); opened = 0 } + +test('checkbox is off and nothing auto-opens by default', () => { + initBox() + assert.equal(checkbox().prop('checked'), false) + getOptions().open() + assert.equal(opened, 0) +}) + +test('ticking the checkbox persists the preference', () => { + initBox() + checkbox().prop('checked', true).trigger('change') + assert.equal(ctx.window.localStorage.getItem(KEY), '1') +}) + +test('when the preference is on, selecting the tab opens the popup', () => { + ctx.window.localStorage.setItem(KEY, '1') + initBox() + assert.equal(checkbox().prop('checked'), true, 'checkbox restored from storage') + closePopups() + getOptions().open() + assert.equal(opened, 1) +}) + +test('the preference survives a reload (a rebuild of the box)', () => { + ctx.window.localStorage.setItem(KEY, '1') + initBox() + initBox() // reload + assert.equal(checkbox().prop('checked'), true) + closePopups() + getOptions().open() + assert.equal(opened, 1) +}) + +test('unticking clears the preference and stops auto-open', () => { + ctx.window.localStorage.setItem(KEY, '1') + initBox() + checkbox().prop('checked', false).trigger('change') + assert.equal(ctx.window.localStorage.getItem(KEY), '0') + initBox() + assert.equal(checkbox().prop('checked'), false) + closePopups() + getOptions().open() + assert.equal(opened, 0) +}) + +test('open() returns false so it does not swallow the panel-show', () => { + initBox() + closePopups() + assert.equal(getOptions().open(), false) +}) + +test('re-selecting the tab focuses the open popup, never re-opens or re-navigates it', () => { + ctx.window.localStorage.setItem(KEY, '1') + initBox() + closePopups() + popups = [] + + getOptions().open() + assert.equal(opened, 1, 'first selection opens it') + const popup = popups[0] + assert.equal(popup.location, '', 'not navigated (authorize URL never built here)') + + getOptions().open() // leave to another tab, come back + getOptions().open() + assert.equal(opened, 1, 'no second popup') + assert.equal(popup.focused, 2, 'focused each return') + assert.equal(popup.location, '', 'never re-navigated') +}) + +test('there is no windowclose handler -- the popup outlives the panel', () => { + initBox() + assert.equal(getOptions().close, undefined) +}) + +test('once the user closes the popup, the tab opens a fresh one', () => { + ctx.window.localStorage.setItem(KEY, '1') + initBox() + closePopups() + popups = [] + getOptions().open() + popups[0].closed = true + getOptions().open() + assert.equal(opened, 2, 'a closed popup is replaced, not focused') + assert.equal(popups.length, 2) +}) + +test('a localStorage that throws (private mode) does not break the tab', () => { + const real = Object.getOwnPropertyDescriptor(ctx.window, 'localStorage') + Object.defineProperty(ctx.window, 'localStorage', + { get() { throw new Error('denied') }, configurable: true }) + assert.doesNotThrow(() => { initBox(); getOptions().open() }) + Object.defineProperty(ctx.window, 'localStorage', real) +}) diff --git a/test/test_banks.py b/test/test_banks.py new file mode 100644 index 000000000..2da07013a --- /dev/null +++ b/test/test_banks.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for /banks/save (BankSave) and /banks/ (BankLoad), +mod/webserver.py. + +conftest.py pre-seeds DATA_DIR/banks.json with "[]" (mimicking +check_environment(), which real prepare() calls but this sandbox never +does). Both handlers use mod.settings.USER_BANKS_JSON_FILE, so this module +snapshots/restores that file around every test. +""" + +import json +import os + +import pytest + +from base import ModUITestCase + + +@pytest.fixture(autouse=True) +def _snapshot_banks_json(): + from mod import settings + + with open(settings.USER_BANKS_JSON_FILE, "r") as fh: + before = fh.read() + + yield + + with open(settings.USER_BANKS_JSON_FILE, "w") as fh: + fh.write(before) + + +class TestBankLoadEmpty(ModUITestCase): + __test__ = True + + def test_load_with_empty_banks_json_returns_empty_list(self): + response, body = self.fetch_json("/banks/") + self.assertEqual(response.code, 200) + self.assertEqual(body, []) + + +class TestBankSave(ModUITestCase): + __test__ = True + + def test_save_writes_banks_json_and_returns_true(self): + banks = [{"title": "Bank 1", "pedalboards": []}] + response = self.fetch("/banks/save", method="POST", body=json.dumps(banks)) + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"true") + + from mod import settings + + with open(settings.USER_BANKS_JSON_FILE, "r") as fh: + on_disk = json.load(fh) + self.assertEqual(on_disk, banks) + + +class TestBankRoundTripNonexistentPedalboard(ModUITestCase): + __test__ = True + + def test_save_bank_referencing_missing_pedalboard_is_filtered_on_load(self): + # DP1/round-trip case from the spec: does BankLoad filter out a + # pedalboard whose bundle path doesn't exist in the sandboxed temp + # pedalboards dir (which is always empty here)? + banks = [ + { + "title": "Bank 1", + "pedalboards": [ + {"bundle": "/nonexistent/thing.pedalboard", "title": "Ghost"} + ], + } + ] + save_response = self.fetch( + "/banks/save", method="POST", body=json.dumps(banks) + ) + self.assertEqual(save_response.body, b"true") + + load_response, body = self.fetch_json("/banks/") + self.assertEqual(load_response.code, 200) + + # The bank itself survives, but its pedalboards list is filtered + # empty: list_banks() (mod/bank.py) drops any pedalboard whose + # 'bundle' path doesn't os.path.exists(), independent of whether it + # is "broken" -- and the temp pedalboards dir is always empty in + # this sandbox, so get_all_pedalboards() never repopulates it + # either. + self.assertEqual(len(body), 1) + self.assertEqual(body[0]["title"], "Bank 1") + self.assertEqual(body[0]["pedalboards"], []) + + # Observed side effect: list_banks() auto-rewrites banks.json to + # drop the now-filtered pedalboard entry as a side effect of a GET + # request (mod/bank.py:58-59, changed=True + shouldSave=True by + # default). So a plain GET /banks/ is not read-only on disk. + from mod import settings + + with open(settings.USER_BANKS_JSON_FILE, "r") as fh: + on_disk = json.load(fh) + self.assertEqual(on_disk[0]["pedalboards"], []) diff --git a/test/test_config.py b/test/test_config.py new file mode 100644 index 000000000..a898094fe --- /dev/null +++ b/test/test_config.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for the small config/user-data POST endpoints: +favorites (mod/webserver.py:FavoritesAdd/FavoritesRemove), /config/set +(SaveSingleConfigValue), /save_user_id/ (SaveUserId) and /auth/nonce +(AuthNonce). + +These all mutate either an in-process singleton (gState.favorites) or a +DATA_DIR JSON file (favorites.json, prefs.json, user-id.json). Since +conftest.py's autouse `user_files` fixture only resets USER_FILES_DIR, this +module snapshots/restores gState.favorites and the DATA_DIR files itself so +tests stay order-independent (per characterization-phase-2.md ground rule 5). +""" + +import json +import os + +import pytest + +from base import ModUITestCase + + +def _read_or_none(path): + if not os.path.exists(path): + return None + with open(path, "r") as fh: + return fh.read() + + +def _restore(path, original): + if original is None: + if os.path.exists(path): + os.remove(path) + else: + with open(path, "w") as fh: + fh.write(original) + + +@pytest.fixture(autouse=True) +def _snapshot_data_dir_state(): + import mod.webserver as webserver + from mod import settings + + favorites_snapshot = list(webserver.gState.favorites) + prefs_before = _read_or_none(settings.PREFERENCES_JSON_FILE) + user_id_before = _read_or_none(settings.USER_ID_JSON_FILE) + favorites_file_before = _read_or_none(settings.FAVORITES_JSON_FILE) + + yield + + webserver.gState.favorites[:] = favorites_snapshot + _restore(settings.PREFERENCES_JSON_FILE, prefs_before) + _restore(settings.USER_ID_JSON_FILE, user_id_before) + _restore(settings.FAVORITES_JSON_FILE, favorites_file_before) + + +class TestFavoritesAdd(ModUITestCase): + __test__ = True + + def test_add_writes_favorites_json_and_returns_true(self): + response = self.fetch( + "/favorites/add", method="POST", body="uri=http%3A%2F%2Fexample.org%2Ffx" + ) + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"true") + + from mod import settings + + with open(settings.FAVORITES_JSON_FILE, "r") as fh: + data = json.load(fh) + self.assertEqual(data, ["http://example.org/fx"]) + + def test_add_duplicate_returns_false_and_does_not_duplicate(self): + body = "uri=http%3A%2F%2Fexample.org%2Ffx" + first = self.fetch("/favorites/add", method="POST", body=body) + self.assertEqual(first.body, b"true") + + second = self.fetch("/favorites/add", method="POST", body=body) + self.assertEqual(second.code, 200) + self.assertEqual(second.body, b"false") + + from mod import settings + + with open(settings.FAVORITES_JSON_FILE, "r") as fh: + data = json.load(fh) + self.assertEqual(data, ["http://example.org/fx"]) + + def test_add_missing_uri_argument_is_400(self): + response = self.fetch("/favorites/add", method="POST", body="") + self.assertEqual(response.code, 400) + + +class TestFavoritesRemove(ModUITestCase): + __test__ = True + + def test_add_then_remove_empties_favorites_json(self): + body = "uri=http%3A%2F%2Fexample.org%2Ffx" + self.fetch("/favorites/add", method="POST", body=body) + + response = self.fetch("/favorites/remove", method="POST", body=body) + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"true") + + from mod import settings + + with open(settings.FAVORITES_JSON_FILE, "r") as fh: + data = json.load(fh) + self.assertEqual(data, []) + + def test_remove_unknown_uri_returns_false(self): + response = self.fetch( + "/favorites/remove", + method="POST", + body="uri=http%3A%2F%2Fnever-added.example%2Ffx", + ) + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"false") + + +class TestSaveSingleConfigValue(ModUITestCase): + __test__ = True + + def test_set_writes_prefs_json_and_returns_true(self): + response = self.fetch( + "/config/set", method="POST", body="key=some-key&value=some-value" + ) + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"true") + + from mod import settings + + with open(settings.PREFERENCES_JSON_FILE, "r") as fh: + data = json.load(fh) + # Values are stored verbatim as whatever get_argument() returned: + # tornado decodes form params to str, so both key and value land as + # plain strings in prefs.json (no int/bool coercion happens here). + self.assertEqual(data["some-key"], "some-value") + self.assertIsInstance(data["some-key"], str) + + def test_set_missing_key_argument_is_400(self): + response = self.fetch("/config/set", method="POST", body="value=x") + self.assertEqual(response.code, 400) + + +class TestSaveUserId(ModUITestCase): + __test__ = True + + def test_save_writes_user_id_json_and_returns_true(self): + response = self.fetch( + "/save_user_id/", + method="POST", + body="name=Ada&email=ada%40example.org", + ) + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"true") + + from mod import settings + + with open(settings.USER_ID_JSON_FILE, "r") as fh: + data = json.load(fh) + self.assertEqual(data, {"name": "Ada", "email": "ada@example.org"}) + + def test_save_missing_name_argument_is_400(self): + response = self.fetch( + "/save_user_id/", method="POST", body="email=ada%40example.org" + ) + self.assertEqual(response.code, 400) + + +class TestAuthNonce(ModUITestCase): + __test__ = True + + def test_auth_nonce_observed_behavior(self): + # Spec: "if the crypto `token` module is None -> {}; otherwise may + # error -- OBSERVE and pin actual." In this dev sandbox + # mod.communication.token imports fine (no exception at import + # time), so `token` is NOT None; but MOD_DEVICE_KEY / MOD_DEVICE_TAG + # are unset (conftest.py never sets them), so + # mod.communication.device.get_tag() raises "Missing device tag" + # once AuthNonce actually calls token.create_token_message(). That + # exception is uncaught by the handler, so tornado turns it into a + # 500. + response = self.fetch( + "/auth/nonce", method="POST", body=json.dumps({"nonce": "abc123"}) + ) + self.assertEqual(response.code, 500) diff --git a/test/test_cors.py b/test/test_cors.py new file mode 100644 index 000000000..9bfc5972d --- /dev/null +++ b/test/test_cors.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for CORS headers. + +Pins the constraint that shapes the Tone3000 download design: ordinary +JsonRequestHandler routes (e.g. /files/list) send no +Access-Control-Allow-Origin at all, while RemoteRequestHandler subclasses +(e.g. Hello, at /hello) echo it back only for the mod.audio/moddevices.com +allow-list (mod/webserver.py:279-292). +""" + +from base import ModUITestCase + + +class TestFilesListCors(ModUITestCase): + __test__ = True + + def test_files_list_has_no_cors_header(self): + response = self.fetch("/files/list?types=bogus") + self.assertEqual(response.code, 200) + self.assertNotIn("Access-Control-Allow-Origin", response.headers) + + +class TestHelloAllowedOriginCors(ModUITestCase): + """Base case for the allow-list: subclasses only change ``origin``.""" + + __test__ = True + origin = "https://mod.audio" + + def test_hello_echoes_allowed_origin(self): + response = self.fetch("/hello", headers={"Origin": self.origin}) + self.assertEqual(response.code, 200) + self.assertEqual( + response.headers.get("Access-Control-Allow-Origin"), + self.origin, + ) + + +class TestHelloAllowedOriginModdevicesCors(TestHelloAllowedOriginCors): + origin = "https://moddevices.com" + + +class TestHelloAllowedOriginPlainHttpCors(TestHelloAllowedOriginCors): + origin = "http://mod.audio" + + +class TestHelloAllowedOriginModAudioSubdomainCors(TestHelloAllowedOriginCors): + origin = "https://pedalboards.mod.audio" + + +class TestHelloAllowedOriginModdevicesSubdomainCors(TestHelloAllowedOriginCors): + origin = "https://cloud.moddevices.com" + + +class TestHelloForeignOriginCors(ModUITestCase): + __test__ = True + + def test_hello_omits_header_for_foreign_origin(self): + response = self.fetch("/hello", headers={"Origin": "https://evil.example"}) + self.assertEqual(response.code, 200) + self.assertNotIn("Access-Control-Allow-Origin", response.headers) diff --git a/test/test_effects_lilv.py b/test/test_effects_lilv.py new file mode 100644 index 000000000..6262d40c0 --- /dev/null +++ b/test/test_effects_lilv.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for the /effect/* plugin-info handlers +(mod/webserver.py: EffectList, EffectGet, EffectGetNonCached, EffectBulk, +EffectAdd) that dereference the global lilv world (utils/utils_lilv.cpp +W/PLUGINS, initialized by modtools.utils.init()). + +Phase 6 (docs/characterization-phase-6.md): this whole module is the +controlled, opt-in exception to the test/conftest.py NEVER-CALL LIST entry +for these exact routes. Every test here runs against an EMPTY lilv world +(LV2_PATH pointed at a dedicated empty sandbox dir in test/conftest.py) -- +no real plugin is ever scanned, so every URI used below is, from lilv's +point of view, unknown/nonexistent. + +Opt-in via the `lilv` pytest marker (see pytest.ini): `pytestmark` below +marks every test in this module, and pytest.ini's `addopts = -m "not lilv"` +excludes the whole module from the default `pytest` run. Run explicitly with +`pytest -m lilv`. + +modtools.utils.init() is the exact function mod/webserver.py's lv2_init() +(aliased import at webserver.py:52, called inside prepare() at :2451) calls +-- confirmed by reading modtools/utils.py's init() (:708), which is a bare +`utils.init()` ctypes call into utils/utils_lilv.cpp init() (:3893: +lilv_world_free + lilv_world_new + lilv_world_load_all + namespace setup, +no JACK calls). This fixture calls the same modtools.utils.init(), skipping +the rest of webserver.prepare() (HMI/host/JACK setup), matching the spec's +guidance to call the same underlying function without the surrounding +production bootstrap. + +No de-init: modtools.utils.cleanup() exists but is deliberately not called +here -- it frees the global lilv world (W = nullptr), and any other test +process code that runs after it (including non-lilv tests, if ever run in +the same process) would then be back to square one, NULL-W, for any /effect/* +call. Leaving the world initialized-but-empty for the rest of the process +is the safer of two bad options and is exactly why this suite keeps these +tests opt-in/marker-gated rather than trying to isolate them automatically. + +The stretch-goal hand-written-.lv2-bundle tests (docs/characterization- +phase-6.md) live in a SEPARATE file, test/test_effects_lilv_fixture.py, +under a SEPARATE marker (`lilv_fixture`), run as its own invocation +(`pytest -m lilv_fixture`) -- NEVER combined in the same process as this +module's `lilv_world` fixture. Reason (load-bearing finding, see that +file's docstring): modtools.utils.init() is safe to call once per process, +but calling it a SECOND time (e.g. to reload with a bundle present) does +not correctly reset lilv's NamespaceDefinitions::getStaticInstance() +singleton (utils/utils_lilv.cpp ~:540), which is a process-global object +independent of the LilvWorld it was last init()ed with. A second init() +call left get_plugin_info()'s ports/category silently empty (instead of +the real data) and reliably crashed the interpreter on process exit +(SIGSEGV during native static/global teardown) -- confirmed by direct +reproduction, not a guess. One init() per process is a hard constraint of +this C extension as used here. +""" + +import json + +from base import ModUITestCase + +import pytest + +pytestmark = pytest.mark.lilv + + +@pytest.fixture(scope="module", autouse=True) +def lilv_world(): + from modtools import utils + utils.init() + yield + # Deliberately no cleanup()/de-init -- see module docstring. + + +class TestEffectList(ModUITestCase): + __test__ = True + + def test_empty_world_returns_empty_list(self): + response = self.fetch("/effect/list") + self.assertEqual(response.code, 200) + content_type = response.headers.get("Content-Type", "") + self.assertIn("application/json", content_type) + self.assertEqual(json.loads(response.body.decode("utf-8")), []) + + +class TestEffectGet(ModUITestCase): + __test__ = True + + def test_unknown_uri_is_404(self): + # get_plugin_info() (modtools/utils.py) raises when the C lookup + # returns NULL for an unknown URI; EffectGet's bare `except:` around + # it turns that into web.HTTPError(404). CachedJsonRequestHandler + # does not override write_error, so this is tornado's default HTML + # error page, NOT a JSON body -- unlike every other handler in this + # suite, do not decode this response as JSON. + response = self.fetch("/effect/get?uri=urn:nonexistent") + self.assertEqual(response.code, 404) + + +class TestEffectGetNonCached(ModUITestCase): + __test__ = True + + def test_unknown_uri_is_404(self): + # Same shape as EffectGet: get_non_cached_plugin_info() raises for + # an unknown URI, caught by the handler's bare `except:`, re-raised + # as web.HTTPError(404) -> tornado's default HTML error page. + response = self.fetch("/effect/get_non_cached?uri=urn:nonexistent") + self.assertEqual(response.code, 404) + + +class TestEffectBulk(ModUITestCase): + __test__ = True + + def _post_bulk(self, uris): + return self.fetch( + "/effect/bulk/", + method="POST", + headers={"Content-Type": "application/json"}, + body=json.dumps(uris), + ) + + def test_empty_list_returns_empty_object(self): + response = self._post_bulk([]) + self.assertEqual(response.code, 200) + content_type = response.headers.get("Content-Type", "") + self.assertIn("application/json", content_type) + self.assertEqual(json.loads(response.body.decode("utf-8")), {}) + + def test_unknown_uris_are_silently_skipped(self): + # EffectBulk.post() calls get_plugin_info(uri) per URI inside a bare + # try/except that `continue`s on failure -- an unknown URI simply + # never makes it into the result dict, no error surfaced at all. + response = self._post_bulk(["urn:nonexistent-1", "urn:nonexistent-2"]) + self.assertEqual(response.code, 200) + self.assertEqual(json.loads(response.body.decode("utf-8")), {}) + + def test_missing_content_type_is_501(self): + # EffectBulk.prepare() requires "application/json" in Content-Type, + # else raises web.HTTPError(501) before post() ever runs. + response = self.fetch("/effect/bulk/", method="POST", body=json.dumps([])) + self.assertEqual(response.code, 501) + + +class TestEffectAdd(ModUITestCase): + __test__ = True + + def test_unknown_uri_ends_in_404_not_false(self): + # SPEC DEVIATION (docs/characterization-phase-6.md guessed "false?"): + # observed behavior is HTTP 404, not a JSON `false` body. Traced: + # 1. EffectAdd.get() does `ok = yield gen.Task(SESSION.web_add, ...)`. + # 2. web_add -> Host.add_plugin -> FakeHost.send_modified(msg, host_callback) + # (mod/development.py) calls host_callback(True) unconditionally + # and synchronously -- no real mod-host round trip. + # 3. Inside add_plugin's host_callback, `resp` is `True`; the guard + # is `if resp < 0`, and `True < 0` is False in Python, so it does + # NOT bail out early despite the URI being unknown. + # 4. It then calls get_plugin_info_essentials(uri) (modtools/utils.py), + # which is NULL-safe: an unknown URI returns a defaults dict + # (`{'error': True, 'controlInputs': [], ...}`) instead of + # raising. So add_plugin finishes normally, registers a fake + # plugin instance in SESSION.host.plugins, and calls + # callback(True) -- i.e. `ok` is True back in EffectAdd.get(). + # 5. Because ok is truthy, EffectAdd.get() proceeds to + # `data = get_plugin_info(uri)` (NOT get_plugin_info_essentials) + # -- this one DOES raise for an unknown URI (see TestEffectGet + # above), caught by EffectAdd's own bare `except:`, which raises + # web.HTTPError(404). + # Net effect: an unknown-URI add is NOT rejected up front; it silently + # registers bookkeeping state on SESSION.host before failing late, + # with a 404 (tornado's default HTML error page) as the only visible + # signal to the caller -- no JSON, no `false`. + response = self.fetch( + "/effect/add/lilv_phase6_unknown?uri=urn:nonexistent&x=0&y=0" + ) + self.assertEqual(response.code, 404) diff --git a/test/test_effects_lilv_fixture.py b/test/test_effects_lilv_fixture.py new file mode 100644 index 000000000..5bb357d75 --- /dev/null +++ b/test/test_effects_lilv_fixture.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Phase 6 (docs/characterization-phase-6.md) STRETCH GOAL: a minimal +hand-written .lv2 bundle (test/fixtures/phase6-fixture.lv2/ -- manifest.ttl ++ phase6-fixture.ttl, deliberately NO binary/.so file), scanned into its own +lilv world, to pin one real /effect/list entry shape and one real +/effect/get field set -- as opposed to test_effects_lilv.py, which only +exercises an empty world / unknown URIs. + +Confirmed: lilv does NOT need the plugin binary to exist on disk for +metadata listing or get_plugin_info(). lilv_world_load_all() only RDF-parses +manifest.ttl and the rdfs:seeAlso'd turtle file; it never dlopen()s +lv2:binary at scan time (only real audio instantiation would). manifest.ttl +here points lv2:binary at "phase6-fixture.so", a file that is never created +anywhere -- the scan and both endpoints below work anyway. So the spec's +"if lilv refuses a binaryless bundle, drop the stretch" escape hatch was not +needed for the *lilv* half of this stretch goal. + +THIS FILE MUST BE RUN AS ITS OWN INVOCATION: `pytest -m lilv_fixture`, never +combined with `-m lilv` (test_effects_lilv.py) in the same process, and +never via a marker expression that would pull both in (e.g. do NOT run +`pytest -m "lilv or lilv_fixture"`). This is a hard constraint, not +caution for its own sake -- it is the actual reason the stretch goal ended +up in its own file instead of a second test class inside +test_effects_lilv.py (which is where it lived during development). + +What went wrong when it *was* a second class in the same file/process, +reproduced directly: that class's setUpClass copied the fixture bundle into +a fresh directory, repointed LV2_PATH, and called modtools.utils.init() a +SECOND time in the process (the module-scoped `lilv_world` fixture in +test_effects_lilv.py having already called it once for the empty world). +modtools/utils.py init() is a bare ctypes call into utils/utils_lilv.cpp +init() (:3893), which does free the previous LilvWorld and build a new one +-- but it also re-runs `NamespaceDefinitions::getStaticInstance(W).init(W)` +(:3898). getStaticInstance() (~:540) is a process-wide C++ singleton +(function-local `static`), NOT reset or freed between the two init() calls +(cleanup() -- the only code path that calls +`NamespaceDefinitions::...cleanup()` -- is, per the phase-6 spec, never +called in this suite). The result, reproduced directly: the second world's +plugin was found and get_all_plugins()/get_plugin_info() returned it, but +its `ports` and `category` fields were silently empty (`[]`) instead of the +real port/category data -- no exception, no crash at that point, just wrong +data, because the namespace lookups the C++ code uses to classify ports/ +categories were resolved against the FIRST (already-freed) world's nodes. +Worse, the process then reliably SIGSEGV'd during native static/global +destructor teardown at interpreter exit (after pytest had already printed +its full PASSED/FAILED summary -- so a naive glance at "tests passed" would +have missed it; `timeout ... ; echo $?` showed "dumped core"). This is +exactly the DANGER-level failure mode the phase-6 spec's NEVER-CALL-LIST +reasoning warns about, just triggered by a second init() instead of a +pre-init call. + +The fix applied: this module now calls modtools.utils.init() exactly ONCE, +as the ONLY init() call in its own process, matching the one-call invariant +that makes test_effects_lilv.py itself safe. +""" + +import json +import os +import shutil + +from base import ModUITestCase + +import pytest + +pytestmark = pytest.mark.lilv_fixture + +_FIXTURE_BUNDLE_SRC = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "fixtures", "phase6-fixture.lv2" +) +_FIXTURE_PLUGIN_URI = "http://example.org/plugins/phase6-fixture" + + +@pytest.fixture(scope="module", autouse=True) +def lilv_world_with_fixture_bundle(): + from conftest import _TEST_ROOT + from modtools import utils + + scan_dir = os.path.join(_TEST_ROOT, "lv2-path-fixture-bundle") + if os.path.isdir(scan_dir): + shutil.rmtree(scan_dir) + os.makedirs(scan_dir) + shutil.copytree(_FIXTURE_BUNDLE_SRC, os.path.join(scan_dir, "phase6-fixture.lv2")) + + # See module docstring: os.environ["LV2_PATH"] is read by lilv itself, + # inside init(), at call time (test/conftest.py sets an initial empty-dir + # value for test_effects_lilv.py; here we override it before this + # module's ONE AND ONLY init() call in this process). + os.environ["LV2_PATH"] = scan_dir + utils.init() + yield + # No cleanup()/de-init -- same reasoning as test_effects_lilv.py, and + # moot here since this is the last thing this process ever does. + + +class TestEffectListWithFixtureBundle(ModUITestCase): + __test__ = True + + def test_effect_list_contains_fixture_plugin_mini_shape(self): + response = self.fetch("/effect/list") + self.assertEqual(response.code, 200) + plugins = json.loads(response.body.decode("utf-8")) + + by_uri = {p["uri"]: p for p in plugins} + self.assertIn(_FIXTURE_PLUGIN_URI, by_uri) + + entry = by_uri[_FIXTURE_PLUGIN_URI] + # Pin the PluginInfo_Mini field set (get_all_plugins), not the full + # PluginInfo shape (that's EffectGet, checked separately below). + self.assertEqual(entry["name"], "Phase 6 Fixture") + self.assertEqual(entry["label"], "Phase 6 Fixture") + self.assertEqual(entry["category"], ["Utility"]) + self.assertEqual( + set(entry.keys()), + { + "uri", "name", "brand", "label", "comment", "buildEnvironment", + "category", "microVersion", "minorVersion", "release", + "builder", "licensed", "iotype", "gui", + }, + ) + + +class TestEffectGetWithFixtureBundle(ModUITestCase): + __test__ = True + + def test_effect_get_returns_full_info_for_fixture_plugin(self): + response = self.fetch("/effect/get?uri=" + _FIXTURE_PLUGIN_URI) + self.assertEqual(response.code, 200) + content_type = response.headers.get("Content-Type", "") + self.assertIn("application/json", content_type) + + data = json.loads(response.body.decode("utf-8")) + self.assertTrue(data["valid"]) + self.assertEqual(data["uri"], _FIXTURE_PLUGIN_URI) + self.assertEqual(data["name"], "Phase 6 Fixture") + # lv2:binary in manifest.ttl points at a .so that was never created + # on disk -- confirms the binary is not required for full plugin + # info (only real audio instantiation would need it). + self.assertEqual(data["binary"], "") + self.assertEqual( + [p["symbol"] for p in data["ports"]["audio"]["input"]], ["in"] + ) + self.assertEqual( + [p["symbol"] for p in data["ports"]["audio"]["output"]], ["out"] + ) diff --git a/test/test_files_list.py b/test/test_files_list.py new file mode 100644 index 000000000..866be6ad5 --- /dev/null +++ b/test/test_files_list.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for GET /files/list (mod/webserver.py:FilesList). + +These pin the *current* behavior of the pipeline the Tone3000 download +feature will rely on: a fresh os.walk per request, recursion into +subfolders (needed for zip-pack downloads), sort-by-fullname ordering, and +the exact JSON entry shape. +""" + +from base import ModUITestCase + + +class TestFilesList(ModUITestCase): + __test__ = True + + def test_missing_types_param_is_501(self): + response = self.fetch("/files/list") + self.assertEqual(response.code, 501) + + def test_unknown_type_is_empty_ok(self): + response, body = self.fetch_json("/files/list?types=bogus") + self.assertEqual(response.code, 200) + self.assertEqual(body, {"ok": True, "files": []}) + + def test_nammodel_missing_folder_is_empty_ok(self): + # No NAM Models folder exists at all under USER_FILES_DIR. + response, body = self.fetch_json("/files/list?types=nammodel") + self.assertEqual(response.code, 200) + self.assertEqual(body, {"ok": True, "files": []}) + + def test_nammodel_lists_nam_files_and_excludes_decoys(self): + self.seed("NAM Models/amp.nam") + self.seed("NAM Models/readme.txt") + self.seed("NAM Models/ir.wav") + + response, body = self.fetch_json("/files/list?types=nammodel") + self.assertEqual(response.code, 200) + self.assertTrue(body["ok"]) + + basenames = [f["basename"] for f in body["files"]] + self.assertEqual(basenames, ["amp.nam"]) + + entry = body["files"][0] + self.assertEqual(entry["filetype"], "nammodel") + self.assertTrue(entry["fullname"].endswith("NAM Models/amp.nam")) + self.assertTrue(entry["fullname"].startswith("/"), "fullname should be absolute") + + def test_nammodel_extension_match_is_case_insensitive(self): + self.seed("NAM Models/AMP.NAM") + + response, body = self.fetch_json("/files/list?types=nammodel") + self.assertEqual(response.code, 200) + basenames = [f["basename"] for f in body["files"]] + self.assertEqual(basenames, ["AMP.NAM"]) + + def test_nammodel_recurses_into_subfolders(self): + # Pins DP1: a zip pack unzipped into NAM Models// will list fine. + self.seed("NAM Models/top.nam") + self.seed("NAM Models/MyPack/clean.nam") + self.seed("NAM Models/MyPack/Nested/deep.nam") + + response, body = self.fetch_json("/files/list?types=nammodel") + self.assertEqual(response.code, 200) + basenames = sorted(f["basename"] for f in body["files"]) + self.assertEqual(basenames, ["clean.nam", "deep.nam", "top.nam"]) + + def test_nammodel_results_sorted_by_full_path(self): + # Pins DP2's findability baseline: ordering is decided by full path. + self.seed("NAM Models/zzz.nam") + self.seed("NAM Models/aaa.nam") + self.seed("NAM Models/MyPack/mmm.nam") + + response, body = self.fetch_json("/files/list?types=nammodel") + self.assertEqual(response.code, 200) + fullnames = [f["fullname"] for f in body["files"]] + self.assertEqual(fullnames, sorted(fullnames)) + + def test_nammodel_walk_is_fresh_every_request(self): + # Pins that /files/list is NOT cached server-side: a file dropped in + # between two requests is visible on the second request with no + # extra signal needed (the staleness the Tone3000 feature must + # solve is 100% client-side). + self.seed("NAM Models/first.nam") + + _, body1 = self.fetch_json("/files/list?types=nammodel") + self.assertEqual([f["basename"] for f in body1["files"]], ["first.nam"]) + + self.seed("NAM Models/second.nam") + + _, body2 = self.fetch_json("/files/list?types=nammodel") + self.assertEqual( + sorted(f["basename"] for f in body2["files"]), + ["first.nam", "second.nam"], + ) + + def test_multiple_types_are_unioned_with_own_filetype_tag(self): + self.seed("NAM Models/amp.nam") + self.seed("Reverb IRs/hall.wav") + + response, body = self.fetch_json("/files/list?types=nammodel,ir") + self.assertEqual(response.code, 200) + + by_basename = {f["basename"]: f for f in body["files"]} + self.assertEqual(set(by_basename), {"amp.nam", "hall.wav"}) + self.assertEqual(by_basename["amp.nam"]["filetype"], "nammodel") + self.assertEqual(by_basename["hall.wav"]["filetype"], "ir") diff --git a/test/test_files_upload.py b/test/test_files_upload.py new file mode 100644 index 000000000..89669cafd --- /dev/null +++ b/test/test_files_upload.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Tests for POST /files/upload/ (mod/webserver.py:FilesUpload). + +The route is the one server-side piece of the Tone3000 download: a browser +page fetches the model bytes itself and hands them here to be written under +USER_FILES_DIR. Everything it refuses, it refuses with a plain 400 *before* +touching the disk -- these tests pin both the happy path and each refusal. +""" + +import json +import os +from urllib.parse import urlencode + +from base import ModUITestCase +from conftest import _USER_FILES_DIR + + +def upload_url(filetype="nammodel", folder="My Tone (12)", name="model.nam"): + return "/files/upload/%s?%s" % (filetype, urlencode({"folder": folder, "name": name})) + + +class TestFilesUpload(ModUITestCase): + __test__ = True + + def upload(self, body=b"nam-bytes", content_type="application/octet-stream", **kwargs): + headers = {"Content-Type": content_type} if content_type is not None else None + return self.fetch(upload_url(**kwargs), method="POST", body=body, headers=headers) + + def user_files(self): + found = [] + for root, _, files in os.walk(str(_USER_FILES_DIR)): + found.extend(os.path.join(root, f) for f in files) + return found + + def test_happy_path_writes_and_reports_the_fullname_files_list_shows(self): + response = self.upload(folder="Clean Amp (7)", name="Clean Amp - Standard.nam") + self.assertEqual(response.code, 200) + + body = json.loads(response.body.decode("utf-8")) + self.assertTrue(body["ok"]) + self.assertTrue(body["fullname"].endswith("NAM Models/Clean Amp (7)/Clean Amp - Standard.nam")) + + with open(body["fullname"], "rb") as fh: + self.assertEqual(fh.read(), b"nam-bytes") + + # The reported fullname is byte-for-byte what /files/list will show, + # so a caller can find its own upload in the list. + _, listing = self.fetch_json("/files/list?types=nammodel") + self.assertEqual([f["fullname"] for f in listing["files"]], [body["fullname"]]) + + def test_extension_check_is_case_insensitive_like_files_list(self): + response = self.upload(name="AMP.NAM") + self.assertEqual(response.code, 200) + + def test_unknown_filetype_is_400(self): + self.assertEqual(self.upload(filetype="bogus").code, 400) + + def test_unmapped_filetype_is_400(self): + # "nam" is a real mod:fileTypes value the NAM plugin declares, but the + # category map deliberately returns no folder for it -- only "nammodel" + # (and "aidadspmodel") map to a directory. + self.assertEqual(self.upload(filetype="nam").code, 400) + + def test_wrong_content_type_is_400(self): + # Anything except application/octet-stream is refused; among other + # things this keeps every cross-origin POST behind a CORS preflight + # that mod-ui never answers. + self.assertEqual(self.upload(content_type="text/plain").code, 400) + self.assertEqual(self.upload(content_type="multipart/form-data").code, 400) + + def test_wrong_extension_is_400(self): + self.assertEqual(self.upload(name="model.wav").code, 400) + self.assertEqual(self.upload(name="model").code, 400) + + def test_traversal_in_name_or_folder_is_400_and_writes_nothing(self): + for folder, name in ( + ("..", "model.nam"), # parent dir as the folder + ("a/b", "model.nam"), # separator smuggled into folder + ("pack", "../evil.nam"), # separator smuggled into name + ): + response = self.upload(folder=folder, name=name) + self.assertEqual(response.code, 400, "folder=%r name=%r" % (folder, name)) + + self.assertEqual(self.user_files(), [], "a refused upload must write nothing") + + def test_empty_or_dot_components_are_400(self): + for folder, name in (("", "model.nam"), (".", "model.nam"), + ("pack", ""), ("pack", "."), ("pack", "..")): + response = self.upload(folder=folder, name=name) + self.assertEqual(response.code, 400, "folder=%r name=%r" % (folder, name)) + + self.assertEqual(self.user_files(), []) diff --git a/test/test_host_commands.py b/test/test_host_commands.py new file mode 100644 index 000000000..27ca1cbab --- /dev/null +++ b/test/test_host_commands.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for the handlers that route into +SESSION.host/SESSION.hmi (mod/webserver.py: EffectConnect, EffectDisconnect, +EffectParameterSet, EffectParameterAddress, DashboardClean, SetBufferSize, +ResetXruns, JackGetMidiDevices, JackSetMidiDevices, TrueBypass, +PedalboardTransportSetSyncMode, PedalboardCvAddressingPluginPortAdd). + +Honest caveat: these tests pin the behavior of the mod/webserver.py handler +code under FakeHost/FakeHMI (mod/development.py), NOT the production +mod-host protocol. They are a regression net for webserver.py edits, +nothing more. + +CRITICAL, spec-overriding finding: GET /effect/remove/ HANGS (does +not error, does not time out server-side -- the HTTP response is simply +never sent) when was never registered with SESSION.host.mapper, +which is true of *every* instance name in this sandbox since we cannot call +POST /effect/add (banned -- dereferences the uninitialized global lilv +world, see test/conftest.py NEVER-CALL LIST). Root cause, confirmed by +direct probing with a 4s client-side request_timeout (got HTTP 599 after +the timeout, not a fast response): Host.remove_plugin (mod/host.py:2604, +decorated @gen.coroutine) calls self.mapper.get_id_without_creating(instance) +*before* its own try/except KeyError guard around self.plugins.pop(...). A +nonexistent instance means that lookup itself raises KeyError, which the +@gen.coroutine machinery captures into the returned (never awaited-on) +Future instead of propagating synchronously -- so the handler's +`callback(False)` line is never reached, gen.Task in EffectRemove never +resolves, and the request hangs until the HTTP client's own timeout. This +class does not call GET /effect/remove/* at all; the route has been added to +the test/conftest.py NEVER-CALL LIST with this reasoning. + +Every test below that mutates SESSION.host state relies on the autouse +`pedalboards_dir` fixture (test/conftest.py) to call SESSION.reset() before +and after each test method, EXCEPT sync-mode: SESSION.reset() does not touch +SESSION.host.profile, so the sync-mode test explicitly restores the default +("/none") at the end. +""" + +import json +import urllib.parse + +from base import ModUITestCase + + +class TestEffectConnectDisconnect(ModUITestCase): + __test__ = True + + def test_connect_between_system_ports_returns_true_and_is_idempotent(self): + # "/graph/capture_1" and "/graph/playback_1" are hardware-port + # shortcuts handled entirely inside Host._fix_host_connection_port + # (mod/host.py) without touching the plugin mapper, so they are safe + # "syntactically valid but nonexistent instance" stand-ins per the + # spec's guidance. FakeHost.send_modified (mod/development.py) + # invokes its callback immediately with True regardless of whether + # the ports are real JACK ports -- there is no validation under the + # fakes. + response, body = self.fetch_json( + "/effect/connect/graph/capture_1,/graph/playback_1" + ) + self.assertEqual(response.code, 200) + self.assertIs(body, True) + + # Second call: (port_from, port_to) is now already in + # self.connections, so Host.connect short-circuits to callback(True) + # without going through send_modified again -- still True. + response2, body2 = self.fetch_json( + "/effect/connect/graph/capture_1,/graph/playback_1" + ) + self.assertEqual(response2.code, 200) + self.assertIs(body2, True) + + # Clean up: disconnect so SESSION.host.connections is restored to + # empty (belt-and-braces -- the autouse fixture would clear it too). + response3, body3 = self.fetch_json( + "/effect/disconnect/graph/capture_1,/graph/playback_1" + ) + self.assertEqual(response3.code, 200) + self.assertIs(body3, True) + + def test_disconnect_with_no_active_connections_returns_true(self): + # Spec guessed this might be `false`. Observed: Host.disconnect's + # inner host_callback(ok) *always* calls callback(True) regardless + # of ok (mod/host.py:3422-3436, "always return true" per its own + # comment) -- even the len(self.connections) == 0 short-circuit + # path (host_callback(False)) ends up reporting True to the client. + response, body = self.fetch_json( + "/effect/disconnect/graph/capture_1,/graph/playback_1" + ) + self.assertEqual(response.code, 200) + self.assertIs(body, True) + + +class TestEffectParameterSet(ModUITestCase): + __test__ = True + + def test_parameter_set_short_circuits_true_when_hmi_uninitialized(self): + # FakeHMI.initialized is always False (mod/development.py), so + # EffectParameterSet.post's `if not SESSION.hmi.initialized: + # self.write(True); return` guard fires before the request body is + # even parsed -- an empty body is fine. + response, body = self.fetch_json( + "/effect/parameter/set/", method="POST", body=b"" + ) + self.assertEqual(response.code, 200) + self.assertIs(body, True) + + +class TestEffectParameterAddress(ModUITestCase): + __test__ = True + + def test_address_nonexistent_instance_returns_false(self): + # Unlike EffectRemove, Host.address (mod/host.py:4795) uses + # self.mapper.get_id(instance) -- the *creating* variant, which + # never raises -- so a never-seen instance safely resolves to + # pluginData is None and callback(False) fires synchronously. + body_json = json.dumps( + {"uri": "https://example.org/actuator", "minimum": 0, "maximum": 1, "value": 0.5} + ).encode("utf-8") + response, body = self.fetch_json( + "/effect/parameter/address/graph/nonexistent/gain", + method="POST", + body=body_json, + ) + self.assertEqual(response.code, 200) + self.assertIs(body, False) + + def test_address_missing_uri_is_404(self): + # EffectParameterAddress.post checks `data.get('uri', None) is + # None` before anything else and raises web.HTTPError(404). + response = self.fetch( + "/effect/parameter/address/graph/nonexistent/gain", + method="POST", + body=b"{}", + ) + self.assertEqual(response.code, 404) + + +class TestReset(ModUITestCase): + __test__ = True + + def test_reset_returns_true(self): + # SESSION.reset under FakeHMI (never initialized) takes the + # synchronous reset_host(True) branch straight to Host.reset, and + # FakeHost invokes every send_notmodified callback immediately with + # True. + response, body = self.fetch_json("/reset/") + self.assertEqual(response.code, 200) + self.assertIs(body, True) + + +class TestSetBufferSize(ModUITestCase): + __test__ = True + + def test_128_and_256_return_ok_false_size_zero(self): + # Matches the spec's own prediction: under MOD_DEV_ENVIRONMENT, + # IMAGE_VERSION is None so the /data/jack-buffer-size write is + # skipped, and set_jack_buffer_size (utils/utils_jack.cpp) is a + # null-guarded no-op that returns 0 without a JACK client -- so + # newsize (0) never equals the requested size, and 'ok' is always + # False. + for size in ("128", "256"): + response, body = self.fetch_json( + "/set_buffersize/%s" % size, method="POST", body=b"" + ) + self.assertEqual(response.code, 200) + self.assertEqual(body, {"ok": False, "size": 0}) + + +class TestResetXruns(ModUITestCase): + __test__ = True + + def test_reset_xruns_returns_true(self): + response, body = self.fetch_json("/reset_xruns/", method="POST", body=b"") + self.assertEqual(response.code, 200) + self.assertIs(body, True) + + +class TestJackMidiDevices(ModUITestCase): + __test__ = True + + def test_get_midi_devices_shape_under_fake_host(self): + # No JACK client -> Host.get_midi_ports() (backing + # web_get_midi_device_list) reports no devices; midiAggregatedMode + # defaults to True (mod/host.py:368, Host.__init__). + response, body = self.fetch_json("/jack/get_midi_devices") + self.assertEqual(response.code, 200) + self.assertEqual( + body, + { + "devsInUse": [], + "devList": [], + "names": {}, + "midiAggregatedMode": True, + }, + ) + + def test_set_midi_devices_matching_current_state_is_a_true_noop(self): + # devs=[], midiAggregatedMode=True, midiLoopback=False exactly match + # Host.__init__'s defaults (mod/host.py:368-369), so both the + # "mode changed" and "loopback changed" branches inside + # Host.set_midi_devices are skipped -- a side-effect-free call that + # still returns True (the handler always writes True on success). + body_json = json.dumps( + {"devs": [], "midiAggregatedMode": True, "midiLoopback": False} + ).encode("utf-8") + response, body = self.fetch_json( + "/jack/set_midi_devices", method="POST", body=body_json + ) + self.assertEqual(response.code, 200) + self.assertIs(body, True) + + def test_set_midi_devices_missing_key_is_500(self): + # Spec guessed "echo/true -- observe". Observed: JackSetMidiDevices + # .post does three unguarded dict subscripts (data['devs'], + # data['midiAggregatedMode'], data['midiLoopback']) with no + # try/except -- a body missing any of them raises an uncaught + # KeyError before any yield, which tornado turns into a plain 500, + # not a JSON error shape. + body_json = json.dumps({"devs": []}).encode("utf-8") + response = self.fetch( + "/jack/set_midi_devices", method="POST", body=body_json + ) + self.assertEqual(response.code, 500) + + +class TestTrueBypass(ModUITestCase): + __test__ = True + + def test_truebypass_returns_false_under_fake_jack(self): + # Matches the spec's own guess. set_truebypass_value (utils_jack) + # is null-guarded without a JACK client and its C-side setter + # reports failure, so the handler always writes False under the + # fakes, both channels, both requested states. + response, body = self.fetch_json("/truebypass/Left/true") + self.assertEqual(response.code, 200) + self.assertIs(body, False) + + response2, body2 = self.fetch_json("/truebypass/Right/false") + self.assertEqual(response2.code, 200) + self.assertIs(body2, False) + + +class TestTransportSetSyncMode(ModUITestCase): + __test__ = True + + def test_known_modes_return_true_then_restore_default(self): + # SESSION.reset() does NOT touch SESSION.host.profile, so this test + # explicitly restores the "/none" (internal/default) mode at the + # end to avoid leaking Profile state into later tests. + try: + for mode in ("/none", "/midi_clock_slave", "/link"): + response, body = self.fetch_json( + "/pedalboard/transport/set_sync_mode%s" % mode, + method="POST", + body=b"", + ) + self.assertEqual(response.code, 200) + self.assertIs(body, True) + finally: + self.fetch_json( + "/pedalboard/transport/set_sync_mode/none", method="POST", body=b"" + ) + + def test_invalid_mode_returns_false_not_error(self): + response, body = self.fetch_json( + "/pedalboard/transport/set_sync_mode/bogus", method="POST", body=b"" + ) + self.assertEqual(response.code, 200) + self.assertIs(body, False) + + +class TestCVAddressingPluginPortAdd(ModUITestCase): + __test__ = True + + def test_add_500s_without_an_existing_addressed_plugin_instance(self): + # Spec said "observe". Observed: PedalboardCvAddressingPluginPortAdd + # calls SESSION.web_cv_addressing_plugin_port_add synchronously + # (not yielded), which ends up in + # Host.addr_task_get_plugin_cv_port_op_mode (mod/host.py:970), + # which does self.mapper.get_id_without_creating(instance) on the + # instance embedded in the uri. Since no plugin instance can exist + # in this sandbox (POST /effect/add is banned -- see + # test/conftest.py NEVER-CALL LIST), this *always* raises KeyError + # and always 500s -- there is no well-formed uri that succeeds here + # without lilv. Unlike EffectRemove this is NOT a hang: the + # exception is raised synchronously (no @gen.coroutine boundary + # swallows it), so tornado's normal error path returns a fast 500. + body = urllib.parse.urlencode( + {"uri": "/cv/graph/nonexistent/cvout", "name": "test"} + ) + response = self.fetch( + "/pedalboard/cv_addressing_plugin_port/add", method="POST", body=body + ) + self.assertEqual(response.code, 500) diff --git a/test/test_index.py b/test/test_index.py new file mode 100644 index 000000000..17227b9a2 --- /dev/null +++ b/test/test_index.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for GET / (mod/webserver.py:TemplateHandler). + +This is the page a future Tone3000-tab edit will touch (the #main-menu +trigger-icon cluster). We pin the redirect-without-?v= behavior and, if the +dev-fake environment can render the full page without JACK/hardware, that +the rendered HTML still contains the anchor points the future edit needs. +""" + +from base import ModUITestCase + + +class TestIndexRedirect(ModUITestCase): + __test__ = True + + def test_bare_get_redirects_with_version_query_arg(self): + response = self.fetch("/", follow_redirects=False) + self.assertEqual(response.code, 302) + location = response.headers.get("Location", "") + self.assertIn("v=", location) + + +class TestIndexRender(ModUITestCase): + __test__ = True + + # TemplateHandler.get (mod/webserver.py) is a gen.coroutine that awaits + # SESSION.wait_for_hardware_if_needed -- if that never calls back under + # the dev-fake environment, this test would hang. self.fetch(...) + # applies AsyncTestCase's own timeout (ASYNC_TEST_TIMEOUT env var, + # default 5s) as a guard; conftest/CI can raise it if 5s is too tight. + + def test_get_with_version_renders_index_html(self): + response = self.fetch("/?v=1") + self.assertEqual(response.code, 200) + self.assertIn("text/html", response.headers.get("Content-Type", "")) + + body = response.body.decode("utf-8") + self.assertIn('id="main-menu"', body) + self.assertIn('id="mod-file-manager"', body) diff --git a/test/test_pedalboards.py b/test/test_pedalboards.py new file mode 100644 index 000000000..6252d77e2 --- /dev/null +++ b/test/test_pedalboards.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for the pedalboard lifecycle: list, save, info, +remove, load_bundle (mod/webserver.py: PedalboardList, PedalboardSave, +PedalboardInfo, PedalboardRemove, PedalboardLoadBundle). + +CRITICAL, spec-overriding finding (see docs/characterization-phase-3.md, +which claimed /pedalboard/list and /banks/ are "self-contained lilv calls" +and safe): **GET /pedalboard/list and GET /banks/ SEGFAULT the whole test +process the moment the sandboxed pedalboards dir contains a real, on-disk +pedalboard bundle** (one written by POST /pedalboard/save). Root cause, +confirmed with faulthandler/gdb: modtools.utils.get_all_pedalboards() +(mod/webserver.py:1316 for PedalboardList, :1685 for BankLoad) invalidates +its Python-side cache and calls into utils.get_all_pedalboards() (the +libmod_utils.so C extension), which crashes inside +``NamespaceDefinitions::init(LilvWorldImpl*)`` -> ``lilv_new_uri()`` while +building its lilv world over a *real* bundle -- reproduced identically via +both PedalboardList and BankLoad, deterministically, every time, and +independent of how many times /pedalboard/list was called before (repeated +calls against an *empty* dir never crash; a *single* call against a dir +containing one real saved bundle crashes every time). By contrast, +GET /pedalboard/info/ (single-bundle parse via get_pedalboard_info(), a +different C entry point) and GET /pedalboard/remove/ are safe with a real +bundle present -- confirmed by direct probing with faulthandler enabled. + +Consequence for this suite: no test here calls GET /pedalboard/list or +GET /banks/ while a real bundle exists in the sandboxed pedalboards dir. +The "list now includes TestBoard" step from the phase-3 spec's core +round-trip is NOT executed as a live HTTP call; the bundle's presence is +instead verified directly on disk (os.path), which is what a real GET +/pedalboard/list would enumerate. The bundle is always removed via +GET /pedalboard/remove/ before any subsequent GET /pedalboard/list call in +the same test. + +The autouse `pedalboards_dir` fixture (test/conftest.py) wipes +LV2_PEDALBOARDS_DIR and resets SESSION around every test, so tests are +order-independent regarding on-disk bundles and SESSION.host state. +""" + +import os +import urllib.parse + +from base import ModUITestCase + + +def _quote(bundlepath): + return urllib.parse.quote(bundlepath, safe="") + + +class TestPedalboardListBaseline(ModUITestCase): + __test__ = True + + def test_list_on_empty_sandbox_is_empty_list(self): + # Safe: the sandboxed pedalboards dir is always empty here (no save + # has happened yet in this test). + response, body = self.fetch_json("/pedalboard/list") + self.assertEqual(response.code, 200) + self.assertEqual(body, []) + + +class TestPedalboardSave(ModUITestCase): + __test__ = True + + def test_save_missing_title_is_400(self): + response = self.fetch("/pedalboard/save", method="POST", body="asNew=1") + self.assertEqual(response.code, 400) + + def test_save_missing_asnew_is_400(self): + response = self.fetch("/pedalboard/save", method="POST", body="title=X") + self.assertEqual(response.code, 400) + + def test_save_creates_bundle_on_disk_with_expected_json_shape(self): + import mod.settings as settings + + body = urllib.parse.urlencode({"title": "TestBoard", "asNew": "1"}) + response, payload = self.fetch_json( + "/pedalboard/save", method="POST", body=body + ) + self.assertEqual(response.code, 200) + self.assertEqual( + payload, {"ok": True, "bundlepath": payload["bundlepath"], "title": "TestBoard"} + ) + + bundlepath = payload["bundlepath"] + self.assertTrue(bundlepath.startswith(settings.LV2_PEDALBOARDS_DIR)) + self.assertTrue(bundlepath.endswith(".pedalboard")) + self.assertTrue(os.path.isdir(bundlepath)) + + # A .ttl bundle got written (host.save_state_to_ttl), NOT observed + # through /pedalboard/list (see module docstring -- that would + # segfault) but directly on disk, which is exactly what a real + # /pedalboard/list GET would enumerate. + entries = os.listdir(bundlepath) + self.assertTrue(any(name.endswith(".ttl") for name in entries)) + self.assertIn("manifest.ttl", entries) + + # cleanup: remove before this test method returns, so no real + # bundle is left for the next test even though the autouse fixture + # would also wipe it. + remove_response = self.fetch( + "/pedalboard/remove/?bundlepath=" + _quote(bundlepath) + ) + self.assertEqual(remove_response.body, b"true") + + def test_save_asnew_0_over_fresh_session_behaves_like_asnew_1(self): + # No pedalboard was ever loaded/saved in this session yet, so + # host.pedalboard_path is "" -- host.save()'s "save over existing" + # branch requires a truthy, on-disk, sandbox-rooted pedalboard_path, + # so asNew=0 here takes the same "save new" branch as asNew=1. + body = urllib.parse.urlencode({"title": "FreshBoard", "asNew": "0"}) + response, payload = self.fetch_json( + "/pedalboard/save", method="POST", body=body + ) + self.assertEqual(response.code, 200) + self.assertTrue(payload["ok"]) + self.assertTrue(os.path.isdir(payload["bundlepath"])) + + self.fetch("/pedalboard/remove/?bundlepath=" + _quote(payload["bundlepath"])) + + def test_save_asnew_0_after_asnew_1_overwrites_same_bundle(self): + body1 = urllib.parse.urlencode({"title": "TestBoard", "asNew": "1"}) + _, payload1 = self.fetch_json("/pedalboard/save", method="POST", body=body1) + bundlepath1 = payload1["bundlepath"] + + body2 = urllib.parse.urlencode({"title": "TestBoard", "asNew": "0"}) + _, payload2 = self.fetch_json("/pedalboard/save", method="POST", body=body2) + bundlepath2 = payload2["bundlepath"] + + self.assertEqual(bundlepath1, bundlepath2) + + import mod.settings as settings + + self.assertEqual(os.listdir(settings.LV2_PEDALBOARDS_DIR), [os.path.basename(bundlepath1)]) + + self.fetch("/pedalboard/remove/?bundlepath=" + _quote(bundlepath1)) + + +class TestPedalboardInfo(ModUITestCase): + __test__ = True + + def test_info_on_bogus_bundlepath_is_500(self): + # get_pedalboard_info() raises a bare Exception on failure + # (modtools/utils.py); PedalboardInfo.get() does not catch it, so it + # surfaces as tornado's generic uncaught-exception 500 HTML page -- + # NOT a JsonRequestHandler JSON error body. + response = self.fetch( + "/pedalboard/info/?bundlepath=" + _quote("/nonexistent/thing.pedalboard") + ) + self.assertEqual(response.code, 500) + self.assertIn("text/html", response.headers.get("Content-Type", "")) + + def test_info_on_real_bundle_matches_saved_title(self): + save_body = urllib.parse.urlencode({"title": "TestBoard", "asNew": "1"}) + _, save_payload = self.fetch_json( + "/pedalboard/save", method="POST", body=save_body + ) + bundlepath = save_payload["bundlepath"] + + response, info = self.fetch_json( + "/pedalboard/info/?bundlepath=" + _quote(bundlepath) + ) + self.assertEqual(response.code, 200) + self.assertEqual(info["title"], "TestBoard") + self.assertIn("plugins", info) + self.assertIn("width", info) + self.assertIn("height", info) + + self.fetch("/pedalboard/remove/?bundlepath=" + _quote(bundlepath)) + + +class TestPedalboardRemove(ModUITestCase): + __test__ = True + + def test_remove_bogus_bundlepath_returns_false(self): + response = self.fetch( + "/pedalboard/remove/?bundlepath=" + _quote("/nonexistent/thing.pedalboard") + ) + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"false") + + def test_remove_nonexistent_path_outside_sandbox_returns_false_and_touches_nothing(self): + # PedalboardRemove gates on os.path.exists(bundlepath) before ever + # calling shutil.rmtree -- a nonexistent path outside the sandbox is + # therefore a safe no-op observation, not a real escape attempt. + outside_path = "/tmp/modui-characterization-does-not-exist.pedalboard" + self.assertFalse(os.path.exists(outside_path)) + + response = self.fetch("/pedalboard/remove/?bundlepath=" + _quote(outside_path)) + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"false") + + +class TestPedalboardCoreRoundTrip(ModUITestCase): + __test__ = True + + def test_save_info_remove_round_trip(self): + """The highest-value test (spec's "core round-trip"), adapted for + the /pedalboard/list segfault documented in the module docstring: + the bundle's presence/absence is asserted via the filesystem and + via /pedalboard/info/, never via a live /pedalboard/list call while + the bundle exists. + """ + import mod.settings as settings + + # 1. Baseline: safe, dir is empty (autouse fixture guarantees this). + response, body = self.fetch_json("/pedalboard/list") + self.assertEqual(body, []) + + # 2. Save. + save_body = urllib.parse.urlencode({"title": "TestBoard", "asNew": "1"}) + save_response, save_payload = self.fetch_json( + "/pedalboard/save", method="POST", body=save_body + ) + self.assertEqual(save_response.code, 200) + self.assertTrue(save_payload["ok"]) + bundlepath = save_payload["bundlepath"] + self.assertEqual(save_payload["title"], "TestBoard") + + # 3. "List now includes TestBoard" -- verified on disk (this is + # exactly what get_all_pedalboards() would scan), NOT via a live + # GET /pedalboard/list call, which segfaults with a real bundle + # present (see module docstring). + self.assertTrue(os.path.isdir(bundlepath)) + self.assertEqual( + os.listdir(settings.LV2_PEDALBOARDS_DIR), + [os.path.basename(bundlepath)], + ) + + # 4. Info. + info_response, info = self.fetch_json( + "/pedalboard/info/?bundlepath=" + _quote(bundlepath) + ) + self.assertEqual(info_response.code, 200) + self.assertEqual(info["title"], "TestBoard") + + # 5. Remove. + remove_response = self.fetch( + "/pedalboard/remove/?bundlepath=" + _quote(bundlepath) + ) + self.assertEqual(remove_response.code, 200) + self.assertEqual(remove_response.body, b"true") + self.assertFalse(os.path.exists(bundlepath)) + + # Now the dir is empty again, so a live /pedalboard/list call is + # safe once more (see module docstring: only a NON-empty dir + # segfaults) and should be back to baseline. + final_response, final_body = self.fetch_json("/pedalboard/list") + self.assertEqual(final_response.code, 200) + self.assertEqual(final_body, []) + + +class TestPedalboardLoadBundle(ModUITestCase): + __test__ = True + + def test_load_bundle_on_bogus_bundlepath_returns_ok_false(self): + body = urllib.parse.urlencode( + {"bundlepath": "/nonexistent/thing.pedalboard", "isDefault": "0"} + ) + response, payload = self.fetch_json( + "/pedalboard/load_bundle/", method="POST", body=body + ) + self.assertEqual(response.code, 200) + self.assertEqual(payload, {"ok": False, "name": ""}) + + def test_load_bundle_on_real_saved_bundle_returns_ok_true_with_name(self): + save_body = urllib.parse.urlencode({"title": "TestBoard", "asNew": "1"}) + _, save_payload = self.fetch_json( + "/pedalboard/save", method="POST", body=save_body + ) + bundlepath = save_payload["bundlepath"] + + load_body = urllib.parse.urlencode( + {"bundlepath": bundlepath, "isDefault": "0"} + ) + response, payload = self.fetch_json( + "/pedalboard/load_bundle/", method="POST", body=load_body + ) + self.assertEqual(response.code, 200) + self.assertEqual(payload, {"ok": True, "name": "TestBoard"}) + + # Reset SESSION before cleanup, per phase-3 spec's order-dependence + # warning: SESSION.host is a process-global singleton, so a test + # that loads a pedalboard must reset afterwards. + reset_response = self.fetch("/reset/") + self.assertEqual(reset_response.code, 200) + + self.fetch("/pedalboard/remove/?bundlepath=" + _quote(bundlepath)) diff --git a/test/test_snapshots.py b/test/test_snapshots.py new file mode 100644 index 000000000..566a9e3a3 --- /dev/null +++ b/test/test_snapshots.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for the snapshot endpoints (mod/webserver.py: +SnapshotList, SnapshotName, SnapshotSave, SnapshotSaveAs). + +Snapshots live entirely on SESSION.host (Python lists/dicts) -- unlike the +pedalboard endpoints in test_pedalboards.py, nothing here calls into lilv, +so there is no segfault risk (confirmed by direct probing). + +SESSION.host is a process-global singleton (mod/session.py), and these +handlers mutate it (pedalboard_snapshots, current_pedalboard_snapshot_id). +The autouse `pedalboards_dir` fixture (test/conftest.py) calls +SESSION.reset() before and after every test, so each test method here +starts from a known-fresh state: pedalboard_snapshots == [], and +current_pedalboard_snapshot_id == -1 (mod/host.py Host.__init__ defaults; +SESSION.reset() restores current_pedalboard_snapshot_id to 0 with a single +"Default" snapshot via host.snapshot_clear() -- see the "after /reset" +tests below, which pin that distinction). +""" + +import urllib.parse + +from base import ModUITestCase + + +class TestSnapshotFreshSession(ModUITestCase): + __test__ = True + + def test_list_after_fixture_reset_has_one_default_entry(self): + # NOT {} -- host.snapshot_clear() (invoked by SESSION.reset(), which + # the autouse pedalboards_dir fixture runs before every test) seeds + # exactly one "Default" snapshot at index 0 (host.py:3052-3053). + # See TestSnapshotSaveWithoutAnyReset below for the true "nothing at + # all" shape ({}), reached only by clearing the list by hand. + response, body = self.fetch_json("/snapshot/list") + self.assertEqual(response.code, 200) + self.assertEqual(body, {"0": "Default"}) + + def test_name_with_default_id_after_reset(self): + response, body = self.fetch_json("/snapshot/name?id=0") + self.assertEqual(response.code, 200) + self.assertEqual(body, {"ok": True, "name": "Default"}) + + def test_name_with_out_of_range_id_falls_back_to_default_name(self): + # snapshot_name(idx) returns None for an out-of-range idx, and the + # handler falls back to DEFAULT_SNAPSHOT_NAME ("Default") -- so this + # is NOT a 404/error shape, it's indistinguishable in its "ok": True + # shape from a real snapshot named "Default". + response, body = self.fetch_json("/snapshot/name?id=99") + self.assertEqual(response.code, 200) + self.assertEqual(body, {"ok": True, "name": "Default"}) + + def test_save_after_reset_succeeds_since_reset_seeds_snapshot_0(self): + # SESSION.reset() leaves current_pedalboard_snapshot_id == 0 with a + # real snapshot at index 0 (see host.py snapshot_clear()), so + # SnapshotSave succeeds here -- contrast with + # TestSnapshotSaveWithoutAnyReset below, which pins the *true* + # "nothing to save" shape (current_pedalboard_snapshot_id == -1). + response = self.fetch("/snapshot/save", method="POST", body="") + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"true") + + def test_saveas_creates_new_snapshot_and_appears_in_list(self): + response, payload = self.fetch_json("/snapshot/saveas?title=Foo") + self.assertEqual(response.code, 200) + self.assertEqual(payload["ok"], True) + self.assertEqual(payload["title"], "Foo") + new_id = payload["id"] + + list_response, list_body = self.fetch_json("/snapshot/list") + self.assertEqual(list_response.code, 200) + self.assertEqual(list_body[str(new_id)], "Foo") + # the pre-existing "Default" snapshot (seeded by SESSION.reset()) + # is still present alongside the new one. + self.assertIn("0", list_body) + + +class TestSnapshotSaveWithoutAnyReset(ModUITestCase): + __test__ = True + + def test_save_returns_false_when_current_snapshot_id_is_negative_one(self): + # Pins the "truly nothing to save" shape from a session that has + # never had snapshot_clear() run at all: Host.__init__ (mod/host.py) + # defaults current_pedalboard_snapshot_id to -1, and snapshot_save() + # returns False whenever that index is out of range. We reach this + # state by resetting the underlying attribute directly (the HTTP + # surface has no route that produces it, since even GET /reset + # seeds a "Default" snapshot at id 0 -- see module docstring). + from mod.webserver import SESSION + + SESSION.host.pedalboard_snapshots = [] + SESSION.host.current_pedalboard_snapshot_id = -1 + + response = self.fetch("/snapshot/save", method="POST", body="") + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"false") + + def test_list_is_empty_dict_when_snapshots_list_is_empty(self): + from mod.webserver import SESSION + + SESSION.host.pedalboard_snapshots = [] + + response, body = self.fetch_json("/snapshot/list") + self.assertEqual(response.code, 200) + self.assertEqual(body, {}) + + +class TestSnapshotRoundTripWithRealPedalboard(ModUITestCase): + __test__ = True + + def test_saveas_and_list_after_loading_a_real_pedalboard(self): + # Save a real bundle, load it back (this reseeds + # pedalboard_snapshots with a single "Default" snapshot via + # host.load() -> save_state_snapshots()/snapshots.json handling), + # then exercise snapshot/saveas + snapshot/list against it. + save_body = urllib.parse.urlencode({"title": "TestBoard", "asNew": "1"}) + _, save_payload = self.fetch_json( + "/pedalboard/save", method="POST", body=save_body + ) + bundlepath = save_payload["bundlepath"] + + load_body = urllib.parse.urlencode( + {"bundlepath": bundlepath, "isDefault": "0"} + ) + load_response, load_payload = self.fetch_json( + "/pedalboard/load_bundle/", method="POST", body=load_body + ) + self.assertEqual(load_response.code, 200) + self.assertTrue(load_payload["ok"]) + + list_response, list_body = self.fetch_json("/snapshot/list") + self.assertEqual(list_response.code, 200) + self.assertEqual(list_body, {"0": "Default"}) + + saveas_response, saveas_payload = self.fetch_json( + "/snapshot/saveas?title=Verse" + ) + self.assertEqual(saveas_response.code, 200) + self.assertEqual(saveas_payload["ok"], True) + self.assertEqual(saveas_payload["title"], "Verse") + + list_response2, list_body2 = self.fetch_json("/snapshot/list") + self.assertEqual(list_body2, {"0": "Default", "1": "Verse"}) + + # Order-dependence warning (phase-3 spec): reset SESSION.host before + # removing the bundle, so later tests see a clean session -- not + # via a live /pedalboard/list call (segfaults with a real bundle + # present, see test_pedalboards.py's module docstring), so we go + # straight to /reset/ then remove. + reset_response = self.fetch("/reset/") + self.assertEqual(reset_response.code, 200) + + self.fetch( + "/pedalboard/remove/?bundlepath=" + + urllib.parse.quote(bundlepath, safe="") + ) diff --git a/test/test_system.py b/test/test_system.py new file mode 100644 index 000000000..7ab0058f2 --- /dev/null +++ b/test/test_system.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for /system/info (SystemInfo), /system/prefs +(SystemPreferences), /hello/ (Hello) and /ping/ (Ping), mod/webserver.py. + +None of these write anything -- they only read read-only system paths +(/etc/mod-release/system, /data/*) that don't exist on a plain dev machine, +so no snapshot/restore fixture is needed here. +""" + +from base import ModUITestCase + + +class TestSystemInfo(ModUITestCase): + __test__ = True + + def test_info_defaults_when_no_hardware_descriptor(self): + response, body = self.fetch_json("/system/info") + self.assertEqual(response.code, 200) + + # /etc/mod-hardware-descriptor.json and /etc/mod-release/system + # don't exist on this dev machine, so every hwdesc-derived field + # falls back to "Unknown" and sysdate falls back to "Unknown". + self.assertEqual(body["hwname"], "Unknown") + self.assertEqual(body["architecture"], "Unknown") + self.assertEqual(body["cpu"], "Unknown") + self.assertEqual(body["platform"], "Unknown") + self.assertEqual(body["bin_compat"], "Unknown") + self.assertEqual(body["model"], "Unknown") + self.assertEqual(body["sysdate"], "Unknown") + + self.assertIn("version", body["python"]) + self.assertIn("machine", body["uname"]) + self.assertIn("release", body["uname"]) + self.assertIn("sysname", body["uname"]) + self.assertIn("version", body["uname"]) + + +class TestSystemPreferences(ModUITestCase): + __test__ = True + + def test_prefs_defaults_when_no_data_files(self): + response, body = self.fetch_json("/system/prefs") + self.assertEqual(response.code, 200) + + # SystemPreferences reads hardcoded absolute "/data/..." paths (NOT + # mod.settings.DATA_DIR -- these are never sandboxed by conftest.py, + # but /data doesn't exist on this dev machine so every pref falls + # back to its default). + self.assertEqual(body["jack_buffer_size"], 128) + self.assertEqual(body["jack_mono_copy"], False) + self.assertEqual(body["jack_sync_mode"], False) + self.assertEqual(body["separate_spdif_outs"], False) + # "service_mod_peakmeter" is an OPTION_FILE_NOT_EXISTS pref, so it's + # True exactly when the disable-flag file is absent. + self.assertEqual(body["service_mod_peakmeter"], True) + self.assertEqual(body["service_mod_sdk"], False) + self.assertEqual(body["service_netmanager"], False) + self.assertEqual(body["autorestart_hmi"], False) + # bluetooth_name has no valdef override (defaults to None), so the + # key is present with a JSON null, not omitted. + self.assertIn("bluetooth_name", body) + self.assertIsNone(body["bluetooth_name"]) + + +class TestHello(ModUITestCase): + __test__ = True + + def test_hello_shape(self): + response, body = self.fetch_json("/hello/") + self.assertEqual(response.code, 200) + # No websocket ever connects in this test harness. + self.assertEqual(body["online"], False) + # IMAGE_VERSION is None in this dev sandbox (no /etc/mod-release/system). + self.assertIsNone(body["version"]) + + +class TestPing(ModUITestCase): + __test__ = True + + def test_ping_reports_hmi_offline(self): + response, body = self.fetch_json("/ping/") + self.assertEqual(response.code, 200) + # The fake HMI (mod.development.FakeHMI under MOD_DEV_ENVIRONMENT=1) + # is never .initialized, so web_ping() calls back False synchronously + # -- no 5s gen.with_timeout wait is actually exercised here. + self.assertEqual(body["ihm_online"], False) + self.assertEqual(body["ihm_time"], 0) diff --git a/test/test_templates.py b/test/test_templates.py new file mode 100644 index 000000000..94d9d97df --- /dev/null +++ b/test/test_templates.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for the template/static-file loaders and the +header conventions shared by every handler base class in mod/webserver.py: + +- TemplateLoader (/load_template/.html) +- BulkTemplateLoader (/js/templates.js) +- TimelessStaticFileHandler (any static file, e.g. /js/desktop.js) +- header pinning: JsonRequestHandler (no Date, no cache headers) vs + CachedJsonRequestHandler (Cache-Control + fixed Expires) + +None of these write anything, so no snapshot/restore fixture is needed. +""" + +from base import ModUITestCase + + +class TestTemplateLoader(ModUITestCase): + __test__ = True + + def test_loads_known_template_from_html_include(self): + response = self.fetch("/load_template/pedalboard.html") + self.assertEqual(response.code, 200) + self.assertIn("text/plain", response.headers.get("Content-Type", "")) + self.assertTrue(len(response.body) > 0) + + def test_unknown_template_is_500(self): + # TemplateLoader.get() open()s the file directly with no + # os.path.exists guard, so a missing template surfaces as an + # uncaught FileNotFoundError -> tornado 500, not a 404. + response = self.fetch("/load_template/does_not_exist.html") + self.assertEqual(response.code, 500) + + +class TestBulkTemplateLoader(ModUITestCase): + __test__ = True + + def test_bundles_html_include_into_templates_object(self): + response = self.fetch("/js/templates.js") + self.assertEqual(response.code, 200) + self.assertIn("text/javascript", response.headers.get("Content-Type", "")) + + body = response.body.decode("utf-8") + self.assertIn("TEMPLATES['pedalboard']", body) + + def test_has_cache_control_and_expires_headers(self): + # BulkTemplateLoader can't subclass CachedJsonRequestHandler (it's + # not JSON), so it sets the same two headers by hand -- pin that + # both routes converge on the same cache contract. + response = self.fetch("/js/templates.js") + self.assertEqual( + response.headers.get("Cache-Control"), "public, max-age=31536000" + ) + self.assertEqual( + response.headers.get("Expires"), "Mon, 31 Dec 2035 12:00:00 gmt" + ) + self.assertNotIn("Date", response.headers) + + +class TestTimelessStaticFileHandler(ModUITestCase): + __test__ = True + + def test_static_file_has_no_date_header(self): + response = self.fetch("/js/desktop.js") + self.assertEqual(response.code, 200) + self.assertNotIn("Date", response.headers) + self.assertEqual( + response.headers.get("Cache-Control"), "public, max-age=31536000" + ) + self.assertEqual( + response.headers.get("Expires"), "Mon, 31 Dec 2035 12:00:00 gmt" + ) + + +class TestJsonRequestHandlerHeaders(ModUITestCase): + __test__ = True + + def test_plain_json_route_has_no_date_and_no_cache_headers(self): + response = self.fetch("/files/list?types=bogus") + self.assertEqual(response.code, 200) + self.assertNotIn("Date", response.headers) + self.assertNotIn("Cache-Control", response.headers) + self.assertNotIn("Expires", response.headers) + + +class TestCachedJsonRequestHandlerHeaders(ModUITestCase): + __test__ = True + + def test_pedalboard_image_check_has_cache_headers(self): + # PedalboardImageCheck (CachedJsonRequestHandler) is filesystem-only + # (SESSION.screenshot_generator.check_screenshot just stats a + # thumbnail.png next to the given bundlepath) -- safe to call + # without lilv/JACK, unlike the /effect/* routes on the NEVER-CALL + # list. The other CachedJsonRequestHandler route, EffectGet, does + # need the global lilv world and is skipped (see NEVER-CALL list). + response, body = self.fetch_json( + "/pedalboard/image/check?bundlepath=/nonexistent/thing.pedalboard" + ) + self.assertEqual(response.code, 200) + self.assertEqual(body["status"], -1) + + self.assertEqual( + response.headers.get("Cache-Control"), "public, max-age=31536000" + ) + self.assertEqual( + response.headers.get("Expires"), "Mon, 31 Dec 2035 12:00:00 gmt" + ) + self.assertNotIn("Date", response.headers) diff --git a/test/test_tokens.py b/test/test_tokens.py new file mode 100644 index 000000000..10ea292c0 --- /dev/null +++ b/test/test_tokens.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for /tokens/save, /tokens/get, /tokens/delete +(mod/webserver.py:TokensSave/TokensGet/TokensDelete). + +These read/write DATA_DIR/tokens.conf directly (not via mod.settings, the +handlers build the path from the DATA_DIR global inline), so this module +snapshots/restores that file around every test to stay order-independent. +""" + +import json +import os + +import pytest + +from base import ModUITestCase + + +@pytest.fixture(autouse=True) +def _snapshot_tokens_conf(): + from mod import settings + + tokens_conf = os.path.join(settings.DATA_DIR, "tokens.conf") + existed = os.path.exists(tokens_conf) + before = None + if existed: + with open(tokens_conf, "r") as fh: + before = fh.read() + + yield + + if existed: + with open(tokens_conf, "w") as fh: + fh.write(before) + elif os.path.exists(tokens_conf): + os.remove(tokens_conf) + + +class TestTokensGetMissing(ModUITestCase): + __test__ = True + + def test_get_with_no_file_returns_ok_false(self): + response, body = self.fetch_json("/tokens/get") + self.assertEqual(response.code, 200) + self.assertEqual(body, {"ok": False}) + + +class TestTokensDeleteMissing(ModUITestCase): + __test__ = True + + def test_delete_with_no_file_is_a_noop_returning_true(self): + response = self.fetch("/tokens/delete") + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"true") + + +class TestTokensRoundTrip(ModUITestCase): + __test__ = True + + def _payload(self): + return { + "user_id": "u1", + "access_token": "at1", + "refresh_token": "rt1", + "expires_in_days": 30, + } + + def test_save_then_get_returns_saved_payload_minus_expires(self): + save_response = self.fetch( + "/tokens/save", method="POST", body=json.dumps(self._payload()) + ) + self.assertEqual(save_response.code, 200) + self.assertEqual(save_response.body, b"true") + + from mod import settings + + tokens_conf = os.path.join(settings.DATA_DIR, "tokens.conf") + with open(tokens_conf, "r") as fh: + on_disk = json.load(fh) + # TokensSave pops "expires_in_days" before writing to disk. + self.assertEqual( + on_disk, {"user_id": "u1", "access_token": "at1", "refresh_token": "rt1"} + ) + + get_response, body = self.fetch_json("/tokens/get") + self.assertEqual(get_response.code, 200) + self.assertEqual( + body, + { + "user_id": "u1", + "access_token": "at1", + "refresh_token": "rt1", + "ok": True, + }, + ) + + def test_save_missing_expires_in_days_is_500_and_does_not_write(self): + # TokensSave unconditionally data.pop("expires_in_days") before + # writing -- a payload that omits it raises an uncaught KeyError + # (500), and nothing is written to tokens.conf. + partial = {"user_id": "u1", "access_token": "at1", "refresh_token": "rt1"} + response = self.fetch( + "/tokens/save", method="POST", body=json.dumps(partial) + ) + self.assertEqual(response.code, 500) + + get_response, body = self.fetch_json("/tokens/get") + self.assertEqual(get_response.code, 200) + self.assertEqual(body, {"ok": False}) + + def test_get_missing_a_required_key_is_ok_false(self): + partial = {"user_id": "u1", "access_token": "at1", "expires_in_days": 30} + save_response = self.fetch( + "/tokens/save", method="POST", body=json.dumps(partial) + ) + self.assertEqual(save_response.body, b"true") + + response, body = self.fetch_json("/tokens/get") + self.assertEqual(response.code, 200) + self.assertEqual(body["ok"], False) + self.assertEqual(body["user_id"], "u1") + self.assertEqual(body["access_token"], "at1") + self.assertNotIn("refresh_token", body) + + def test_save_then_delete_then_get_is_ok_false_again(self): + self.fetch("/tokens/save", method="POST", body=json.dumps(self._payload())) + + delete_response = self.fetch("/tokens/delete") + self.assertEqual(delete_response.code, 200) + self.assertEqual(delete_response.body, b"true") + + from mod import settings + + tokens_conf = os.path.join(settings.DATA_DIR, "tokens.conf") + self.assertFalse(os.path.exists(tokens_conf)) + + get_response, body = self.fetch_json("/tokens/get") + self.assertEqual(get_response.code, 200) + self.assertEqual(body, {"ok": False}) diff --git a/test/test_websocket.py b/test/test_websocket.py new file mode 100644 index 000000000..c7214d7f3 --- /dev/null +++ b/test/test_websocket.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for the /websocket connect-time handshake +(mod/webserver.py:ServerWebSocket, mod/session.py:Session.websocket_opened, +mod/host.py:Host.report_current_state -- all wired through +mod/development.py:FakeHost.open_connection_if_needed in this dev harness). + +Pins the message-PUSH sequence a client sees right after connecting: this is +the ``msg_callback`` broadcast path a future Tone3000 "file added" push +notification would ride. + +Exploratory findings (see docs/characterization-phase-5.md, verified against +this sandbox -- FakeHMI uninitialized, empty pedalboard, no CC devices, no +hardware ports, /proc/meminfo present so Host.memtimer is armed): + +- The connect-time push on the FIRST socket of the process is exactly 7 + ordered messages, by PREFIX (first whitespace-delimited word): + sys_stats, stats, transport, truebypass, loading_start, size, loading_end + The spec's sketch guessed a "stop"/ready-ish closing token; the real + ready-marker is ``loading_end `` + (mod/host.py:Host.report_current_state, last line before the per-plugin + addressing/HW sections, which are all empty in this sandbox). +- Immediately AFTER that 7-message burst, the first socket receives one + EXTRA ``sys_stats ...`` message. This is not part of + report_current_state's own push -- it is + FakeHost.open_connection_if_needed (mod/development.py) calling + ``self.memtimer_callback()`` synchronously, once, right after + ``report_current_state()`` returns, on the branch where + ``self.readsock``/``self.writesock`` were previously None (i.e. only for + the connection that actually "establishes" the fake host link). This is a + detail the spec's sketch did not anticipate; tests below treat it as an + unpinned trailing message (present in practice in this sandbox, but not + asserted on) rather than pin an exact total message count, per the phase's + "pin prefixes/arg-counts, not volatile detail" and "consume/ignore extra + stats messages" guidance. +- A SECOND, concurrent connection (opened while the first is still open) + gets the exact same 7-message report_current_state burst (every new + websocket gets its own full state dump -- Session.websocket_opened's + first-vs-not-first branch, session.py:236, only decides whether + Host.start_session runs first; it does not change what gets pushed to the + new socket). The observable difference is narrower than "first socket + session branch": it is FakeHost.open_connection_if_needed's *own* branch + on ``self.readsock is not None and self.writesock is not None`` -- true + for every connection after the first -- which skips + statstimer.start()/memtimer_callback() entirely. So only the very first + socket of the process gets the extra trailing sys_stats; the second socket + does not, even though both get an identical initial 7-message burst. +- PeriodicCallback (mod/host.py Host.statstimer/memtimer) binds + ``IOLoop.current()`` at construction time (tornado 4.3 + ioloop.py:PeriodicCallback.__init__), and SESSION.host is a process-level + singleton constructed at ``mod.webserver`` import time -- before any + per-test IOLoop exists. So ``statstimer.start()``/``memtimer.start()`` + schedule ticks on a *different*, never-pumped IOLoop; in practice no + additional "stats ..."/"sys_stats ..." messages arrive during a test's own + ~2s collection window. Tests still avoid asserting exact trailing counts, + since this is an artifact of import ordering, not a documented guarantee. +- Closing the client side of the connection is NOT synchronously reflected + server-side: right after ``conn.close()`` returns, GET /hello still + reports ``online: true`` (SESSION.websockets still has the entry) because + ``ServerWebSocket.on_close`` -> ``SESSION.websocket_closed`` only runs + once the server's IOLoop actually polls and processes the close frame. + This turned out FLAKY to observe deterministically: a bounded number of + ``gen.moment`` spins (pure callback-queue turns, no real I/O wait) is + sometimes enough and sometimes not. Per the phase spec's own guidance + ("count may update asynchronously; drop the assertion rather than + sleep-loop if flaky"), the close test below does not assert on the + post-close ``online`` value -- it only pins that the server tolerates the + close without erroring and keeps answering HTTP requests. +""" + +from tornado import gen, websocket +from tornado.testing import gen_test + +from base import ModUITestCase + +# Generous but bounded -- nothing here should ever legitimately take this +# long; a real hang (behavior regression) fails fast instead of hanging the +# whole suite. +_READ_TIMEOUT = 5.0 + +# Exact pinned prefix sequence of the connect-time report_current_state +# burst in this sandbox (empty pedalboard, no CC devices, no HW ports). +_EXPECTED_PREFIXES = [ + "sys_stats", + "stats", + "transport", + "truebypass", + "loading_start", + "size", + "loading_end", +] + +_READY_MARKER_PREFIX = "loading_end" + + +class WebSocketTestCase(ModUITestCase): + """Shared helpers. Not collected directly (__test__ stays False).""" + + __test__ = False + + def ws_url(self): + return self.get_url("/websocket").replace("http://", "ws://") + + @gen.coroutine + def read_one(self, conn, timeout=_READ_TIMEOUT): + """Read a single message with a hard timeout so a behavior change + (e.g. the ready marker disappearing) fails fast instead of hanging. + """ + msg = yield gen.with_timeout(self.io_loop.time() + timeout, conn.read_message()) + raise gen.Return(msg) + + @gen.coroutine + def drain_until_ready(self, conn, timeout=_READ_TIMEOUT): + """Collect messages up to and including the ready marker + (``loading_end ...``). Returns the list of messages, ready marker + included. Raises (via with_timeout / assertion) if the marker never + arrives or the socket closes first. + """ + messages = [] + while True: + msg = yield self.read_one(conn, timeout=timeout) + if msg is None: + self.fail( + "server closed the websocket before the ready marker " + "(%r) arrived; messages so far: %r" + % (_READY_MARKER_PREFIX, messages) + ) + messages.append(msg) + if msg.split(" ", 1)[0] == _READY_MARKER_PREFIX: + break + raise gen.Return(messages) + + +class TestWebSocketConnectSequence(WebSocketTestCase): + __test__ = True + + @gen_test(timeout=10) + def test_connect_pushes_pinned_prefix_sequence(self): + conn = yield websocket.websocket_connect(self.ws_url()) + try: + messages = yield self.drain_until_ready(conn) + prefixes = [m.split(" ", 1)[0] for m in messages] + self.assertEqual(prefixes, _EXPECTED_PREFIXES) + finally: + conn.close() + + @gen_test(timeout=10) + def test_transport_message_shape(self): + conn = yield websocket.websocket_connect(self.ws_url()) + try: + messages = yield self.drain_until_ready(conn) + transport_msgs = [m for m in messages if m.split(" ", 1)[0] == "transport"] + self.assertEqual(len(transport_msgs), 1) + + parts = transport_msgs[0].split(" ") + # "transport %i %f %f %s" -- prefix + 4 fields. + self.assertEqual(len(parts), 5) + rolling, bpb, bpm, sync = parts[1:] + + # arg-count + parseability is the load-bearing assertion; the + # concrete defaults below are deterministic Profile() config + # (not volatile like cpu%/timestamps), so pinning them is safe + # in this fresh-sandbox harness. + self.assertEqual(int(rolling), 0) + self.assertEqual(float(bpb), 4.0) + self.assertEqual(float(bpm), 120.0) + self.assertEqual(sync, "none") + finally: + conn.close() + + @gen_test(timeout=10) + def test_truebypass_message_reflects_defaults(self): + conn = yield websocket.websocket_connect(self.ws_url()) + try: + messages = yield self.drain_until_ready(conn) + tb_msgs = [m for m in messages if m.split(" ", 1)[0] == "truebypass"] + self.assertEqual(len(tb_msgs), 1) + + parts = tb_msgs[0].split(" ") + # "truebypass %i %i" -- prefix + 2 fields. + self.assertEqual(len(parts), 3) + left, right = int(parts[1]), int(parts[2]) + # Observed default (Host.last_true_bypass_left/right initial + # state in this dev sandbox): both channels report "true bypass + # on". + self.assertEqual((left, right), (1, 1)) + finally: + conn.close() + + @gen_test(timeout=10) + def test_size_message_present_with_two_numeric_args(self): + conn = yield websocket.websocket_connect(self.ws_url()) + try: + messages = yield self.drain_until_ready(conn) + size_msgs = [m for m in messages if m.split(" ", 1)[0] == "size"] + self.assertEqual(len(size_msgs), 1) + + parts = size_msgs[0].split(" ") + # "size %d %d" -- prefix + 2 fields. + self.assertEqual(len(parts), 3) + width, height = int(parts[1]), int(parts[2]) + # Empty/default pedalboard in a fresh sandbox -> zero size. + self.assertEqual((width, height), (0, 0)) + finally: + conn.close() + + @gen_test(timeout=10) + def test_second_concurrent_connection_gets_full_burst_but_no_trailing_extra(self): + conn1 = yield websocket.websocket_connect(self.ws_url()) + try: + messages1 = yield self.drain_until_ready(conn1) + prefixes1 = [m.split(" ", 1)[0] for m in messages1] + self.assertEqual(prefixes1, _EXPECTED_PREFIXES) + + # The first socket also gets one extra trailing sys_stats + # (FakeHost.open_connection_if_needed's memtimer_callback() + # kick, see module docstring). Consume it so it can't bleed + # into the second connection's read below; don't fail if it + # doesn't show up (it is an unpinned, environment-dependent + # detail). + try: + extra = yield self.read_one(conn1, timeout=1.0) + if extra is not None: + self.assertEqual(extra.split(" ", 1)[0], "sys_stats") + except gen.TimeoutError: + pass + + # Second connection, opened while the first is still open -- + # exercises Session.websocket_opened's "not the first socket" + # branch (session.py:236). + conn2 = yield websocket.websocket_connect(self.ws_url()) + try: + messages2 = yield self.drain_until_ready(conn2) + prefixes2 = [m.split(" ", 1)[0] for m in messages2] + # Same full report_current_state burst as the first socket. + self.assertEqual(prefixes2, _EXPECTED_PREFIXES) + + # But NOT the extra trailing sys_stats: FakeHost's + # readsock/writesock are already set by the time the second + # socket connects, so open_connection_if_needed takes the + # early-return branch (report_current_state only, no + # statstimer/memtimer kick). + with self.assertRaises(gen.TimeoutError): + yield self.read_one(conn2, timeout=1.0) + finally: + conn2.close() + finally: + conn1.close() + + +class TestWebSocketClose(WebSocketTestCase): + __test__ = True + + @gen_test(timeout=10) + def test_clean_close_no_server_error_and_hello_reflects_it(self): + conn = yield websocket.websocket_connect(self.ws_url()) + yield self.drain_until_ready(conn) + + response = yield self.http_client.fetch(self.get_url("/hello/")) + self.assertEqual(response.code, 200) + self.assertIn(b'"online": true', response.body) + + conn.close() + + # DROPPED (per phase-5 spec §6: "count may update asynchronously; + # drop the assertion rather than sleep-loop if flaky"): a strict + # assertion that /hello's `online` flips to false shortly after + # close. websocket_closed (session.py) only runs once the server's + # IOLoop actually polls and processes the close frame -- draining a + # bounded number of `gen.moment`s (pure callback-queue turns, no + # real I/O wait) was NOT enough to observe it in this harness + # (confirmed flaky: failed after 10 moments in a run where the + # exploratory script's 5-moments-after-two-fetches version happened + # to see it flip). A real wait would require an actual sleep/poll, + # which the spec explicitly says not to add. So this test only pins + # what's reliable: the server tolerates a client-initiated close + # without erroring, and a subsequent HTTP request still succeeds + # (well-formed JSON, 200) -- it does not assert on the `online` + # value after close. + response = yield self.http_client.fetch(self.get_url("/hello/")) + self.assertEqual(response.code, 200) + self.assertIn(b'"online"', response.body)