diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..4d0ed353 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: [ ps-1.13, feat/** ] + pull_request: + +# Every step below calls a Makefile target. No test file is named here or in the +# Makefile: `unittest discover` finds test/test_*.py and `node --test` finds +# test/js/**/*.test.js. A new test runs without a change to either file. +# `make test` runs all of it at once and is what you run before you push. + +jobs: + python: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Check that the server files parse + run: make compile + - name: Run the Python tests + run: make test-py + + frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Run the JavaScript tests + run: make test-js + - name: Check the JSDoc types + run: make typecheck diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..9a239e33 --- /dev/null +++ b/Makefile @@ -0,0 +1,60 @@ +# pi-Stomp: the checks of this repository. +# +# Each target finds its own files. Do not write a file name here and do not +# write a file name in .github/workflows/ci.yml. A new test must run without a +# change to this file. +# +# Note for the packaging: debian/rules in pi-gen-pistomp overrides every +# dh_auto_* step, so debhelper does not run these targets. + +PYTHON ?= python3 +NODE ?= node + +# The server files that must compile. py_compile reads one file at a time, so +# this list is not test discovery; it is a syntax gate on the files that the +# device runs first. +PY_COMPILE_FILES = mod/host.py mod/session.py mod/webserver.py mod/sfzbuilder.py setup.py + +# unittest discover reads every test*.py file under test/. +# node --test finds every *.test.js file below the directory that it runs in. +# The recipe changes to JS_TEST_DIR first, because a directory that you give to +# node --test as an argument is read as a file, and a glob needs a new version +# of node. This form works with node 18 and later. +JS_TEST_DIR = test/js + +.PHONY: test test-py test-js typecheck smoke compile utils clean-utils help + +## test: run every check +test: compile test-py test-js typecheck smoke + +## compile: check that the server files parse +compile: + $(PYTHON) -m py_compile $(PY_COMPILE_FILES) + +## test-py: run every Python test under test/ +test-py: + $(PYTHON) -m unittest discover -s test -v + +## test-js: run every JavaScript test under test/js/ +test-js: + cd $(JS_TEST_DIR) && $(NODE) --test + +## typecheck: check the JSDoc types of the islands +typecheck: + npx -y -p typescript@5 tsc --noEmit + +## smoke: draw the SFZ builder in headless Chrome and check what it drew +smoke: + ./test/browser/run.sh + +## utils: build libmod_utils.so +utils: + $(MAKE) -C utils + +## clean-utils: remove the build of libmod_utils.so +clean-utils: + $(MAKE) -C utils clean + +## help: show the targets +help: + @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## / /' diff --git a/docs/frontend-architecture.md b/docs/frontend-architecture.md new file mode 100644 index 00000000..3f94bb10 --- /dev/null +++ b/docs/frontend-architecture.md @@ -0,0 +1,617 @@ +# Frontend Architecture + +Status: in use. The SFZ builder is built this way and continuous integration +holds the rules below. +Branch: `feat/sfzbuilder`. +Date: 2026-09-03. Last change: 2026-09-04. + +--- + +## 1. Purpose + +This document gives the design for new frontend code in mod-ui. +It also gives the reasons for the design. +The SFZ builder is the first feature that uses this design. + +## 2. Technical names + +This document uses these technical names: + +- **Legacy code**: the JavaScript files in `html/js/`. This code is in the global scope. +- **Island**: a group of files for one feature. An island has no global names. +- **Seam**: the one point where the legacy code starts an island. +- **Token**: a CSS custom property that holds one design value. +- **Model**: a file that has only pure functions. A model does not touch the DOM. + +## 3. The problem + +The frontend has 17726 lines of JavaScript in 29 files. +All of this code is in the global scope. +The file `index.html` loads the code with 41 script elements. +The order of these elements is important. + +The repository has no module system and no bundler. +It has no JavaScript tests. +It has no continuous integration. +It has no configuration file for a linter, a formatter or a type checker. + +These conditions cause four problems: + +1. You cannot find the callers of a function, because all names are global. +2. You cannot test the logic, because the logic and the DOM code are in the same functions. +3. You must change `index.html` for each new feature, because that file holds all the markup. +4. The tools cannot help you, because no type checker examines the code. + +The team will not rewrite the legacy code. +The file `pedalboard.js` has 3040 lines. +The file `modgui.js` has 2711 lines. +These files must continue to work. + +## 4. The proposal + +Write each new feature as an island. +Do not change the legacy code. +The legacy code and the islands stay together for a long time. +This is the correct condition. It is not a temporary condition. + +These are the seven rules. + +### 4.1 Use ES modules. Do not use a bundler + +Write islands as ES modules. +The browser loads these modules with the `import` keyword. +Tornado sends JavaScript files with the correct type. + +A bundler needs a `package.json` file in the root directory. +A bundler also needs changes to the device package in `pi-gen-pistomp`. +We do not do this work now. +Write the code so that a bundler is easy to add later. + +### 4.2 Keep one seam + +The legacy code starts an island at one point only. +An island does not read global variables directly. +If an island needs data from the legacy code, write an adapter file. +The adapter is the only file that knows the global names. + +### 4.3 Each island makes its own markup + +Today the markup is in `index.html`. The JavaScript finds the markup by ID. +This is the reason that `index.html` is very large. + +An island gets one empty element from `index.html`. +The island makes all of its other elements. +This rule makes `index.html` smaller with each new feature. + +### 4.4 Use JSDoc types and `// @ts-check` + +Put `// @ts-check` at the top of each island file. +Write the types in JSDoc comments. +The type checker then finds errors in the editor and in continuous integration. + +This method needs no build step. +The files stay as plain JavaScript. The browser loads them directly. +TypeScript gives more, but TypeScript needs a bundler. +A change from JSDoc to TypeScript is mechanical. Do it when a bundler exists. + +### 4.5 Use CSS tokens + +The file `less/variables.less` has 841 lines. +These are LESS variables. LESS variables are constants. They are gone after the compile. + +An island puts its design values in CSS custom properties. +The LESS rules then read these properties. +Custom properties operate at run time. A LESS variable does not exist after the +compile, so nothing can read it and nothing can change it. + +Three things follow from that, and they are the reason for the rule: + +* One value changes every rule that reads it. The accent of the SFZ builder + moved from orange to purple in one line. +* A media query, or a class on the panel, can give a token a different value. + A LESS variable cannot do this at all. +* A test can read a token. See the note on the accent below. + +mod-ui is a web page in a browser -- a laptop, and sometimes a tablet or a +phone. It is **not** what the LCD of the pi-stomp shows: that screen belongs to +the `pi-stomp` application, which talks to this server over WebSocket and MIDI +and draws nothing from `html/`. Do not size a panel for that LCD. + +Put the tokens on `:root`, not on the root element of the island, and start +every name with the name of the island, for example `--sfz-`. +A token on the island cannot be read outside it, and jQuery UI puts the helper +of a drag on the `body`, which is outside it. +A `var()` that does not resolve makes the whole property invalid, so the helper +of the SFZ builder was black text on nothing. +The prefix is what keeps a token at the root from meeting a legacy name. + +A test names a token. A test never names the value of a token. +The smoke test of the SFZ builder held `rgb(242, 148, 70)` in two places, and +both broke on the day the accent changed, although nothing was wrong. +It now reads `--sfz-accent` through a probe element, which also proves the token +reaches the `body`, which is the fault the check was written for. + +### 4.6 Keep the model pure. Keep the view thin + +Put the calculations in a model file. +The model has no DOM code and no network code. +Test the model with the `node:test` module. + +Do not write a unit test for the elements that the view makes. +The cost of these tests is high. The value is low. + +Write one smoke test instead. It draws the panel in headless Chrome and asks the +questions that only a browser can answer: the computed style, where the focus +sits, the tab order, and a drag from a list onto a pad. +`test/browser/` holds it and `make smoke` runs it. +This test is cheap because there is one of it, and it has found faults that no +unit test could reach: a stylesheet that did not load, a value that changed the +width of its own control, and a control that the keyboard could not reach. + +A check in this test says what it wants, not what the code does today. +When a check fails, read it again before you change the panel. Two checks of the +tab order were themselves wrong: one assumed that pad 1 held controls, and one +let the last pad reach past the grid to the end of the panel. + +The page answers its own `confirm`. A headless browser stops on a dialog and the +run does not end, so the test replaces `window.confirm` once at the top, keeps +the answer in a variable, and keeps the question in another. A check that wants +the other answer sets the variable and puts it back. Do the same for `prompt` +and `alert` if a panel ever opens one. + +### 4.7 Make the state explicit + +Keep the state in one object. +Change the state with one function. +The view is a function of the state. + +This is not a framework. This is approximately 30 lines of code. +The result is one place to write log messages. It is also one place to stop the debugger. + +**A control sends what it changed, not the object it was drawn from.** +Some controls must not draw the panel again. The gain of a pad in the SFZ +builder is one: a redraw takes the focus out of the box you are typing in. +The first version let those controls write into the pad object directly, which +kept the panel still but put the change outside the store, so nothing that +watches the store -- the mark that says the work is unsaved, a log line, the +debugger -- ever saw it. It also left each control holding an object that the +store was free to replace. +Send the index and the one field instead, and let the store make the new pad. +A control that changes nothing the state holds -- how many pads fit on a line, +which is how you look at the bank rather than what the bank is -- stays out of +the store and out of the flag. + +**An answer belongs to the view that asked for it.** +An island that asks for three things at once, and lets you change what it is +showing while they are in the air, will get them back out of order. +The SFZ builder asks for the sample list, the files of the bank and the pad +layout in three requests; click bank A and then bank B and the answers for A +can land last and draw the pads of A over the bank of B. +Keep a number that counts the views. Each request keeps the number the panel had +when it went out, and an answer that comes back to a different number is dropped +-- the request that replaced it is already on its way. +Raise the number wherever what the answers are for changes: a new bank, a new +sample source, a bank that goes away. +The smoke test holds one bank's answer back, clicks past it, and asks that the +answer never lands. + +### 4.8 Vendor the dependencies + +Do not use npm for browser code. +Do not load a file from a CDN. The device is frequently offline. + +If an island needs a third-party library, do these steps: + +1. Make sure that the library has an ES module build. +2. Copy the file into `html/js/app/vendor/`. +3. Write the version and the source URL in a comment at the top of the file. +4. Import the file with a relative path. + +Prefer the browser APIs. +A small quantity of your own code is better than a large dependency. +Example: the Web Audio API can calculate the peaks for a waveform. + +### 4.9 Put each new directory in `setup.py` + +`setup.py` gives the file list of the package. +It names each directory of `html/` with a glob that is **not** recursive. + + (('share/mod/html/js'), glob('html/js/*.js')), + +A new directory is thus absent from the package. +The server on the device then gives 404 for each file in it. + +An island makes all of its own markup (rule 4.3). +Thus a file that is absent gives an empty panel and **no other sign**. +This is a hard fault to find. + +When you make an island, do these steps: + +1. Add one line for the code: `glob('html/js/app//*.js')`. +2. Add one line for the style: `glob('html/js/app//*.css')`. +3. Run `python3 -m unittest discover -s test -p "test_setup_data_files.py"`. + +`test/test_setup_data_files.py` runs the same globs that `setup.py` runs and +asks that each file under `html/js/app/` is in one of them. CI runs this test. + +The seam also writes a message on the panel when the island does not load, so +the next fault of this class is visible in the browser. + +### 4.10 Give the panel back to the keyboard + +`main.less` holds this rule: + + * { + outline: 0 !important + } + +It takes the focus ring that the browser draws off every element of the page, +and thus off every island. +A person on the keyboard then has no sign at all of where the focus sits. + +An island puts a ring back, inside itself only: + + #sfzbuilder-library :focus { + outline: 2px solid var(--sfz-focus) !important; + outline-offset: -2px; + } + #sfzbuilder-library :focus:not(:focus-visible) { + outline: 0 !important; + } + +The `!important` is not decoration. Nothing except `!important` beats an +`!important` declaration, and the rule above is the one that has to win. +The offset is negative so that the ring is drawn inside the edge of the element: +a ring outside the edge is cut by any list or grid that scrolls. +The second rule takes the ring off a press, which matters wherever the page is +opened by touch -- a tablet or a phone -- where a tap would else leave a ring +behind. +A browser that does not know `:focus-visible` drops that rule and shows the ring +for a press as well, which is the safe way for it to fail. + +Then three rules about the elements themselves. + +**An element that takes a click must take focus.** +A `div` or a `li` with an `onclick` and no `tabindex` cannot be reached by the +keyboard, and an element that cannot hold the focus cannot show a focus ring. +Give it `tabindex="0"`, a role, and the two keys that activate a button. +`clickable()` in `dom.js` does all three. +Act on a key only when the element itself has the focus: a press on a button +inside it sends a click of its own, and the keydown goes up to the element as +well, so without the test it acts two times. + +**Do not put a role on a container that drops what is inside it.** +The children of `button`, `option`, `tab`, `menuitem`, `checkbox`, `radio` and +`switch` are presentational: a screen reader is free to read the element as one +piece of text and drop every control inside it. +A pad of the SFZ builder holds nine controls and wore `role="button"` for a +while, which hid all nine. +A pad is a `group`. A sample row keeps the `listitem` that a `li` in a `ul` +already has. A bank row holds text only, so it is a `button`. +An element that gains a control while it is open, such as the note stepper when +a click turns it into a field, drops its role while the field is there. + +**Keep the order of the document.** +Do not write a positive `tabindex`. It pulls the element in front of the whole +page. Do not write a negative one either, which takes the control out of reach. +`tabindex="0"` everywhere means the order is the order you drew the elements in. +A long panel is thus a long tab order -- the SFZ builder has about ten stops for +each pad it holds -- and that is accepted for now. A shorter path is a question +of its own, in section 7. + +### 4.11 Take down with jQuery what you put up with jQuery + +An island writes its own elements with the browser API (rule 4.3), and +`clear()` in `dom.js` removes them with `removeChild`. + +This is wrong for an element that carries a jQuery UI widget. +jQuery UI 1.10 keeps a `draggable` and a `droppable` in `$.ui.ddmanager` and in +the jQuery data cache, and it removes them only through the `cleanData` hook of +jQuery, which a native `removeChild` never runs. +The entries thus stay after the element is gone. They hold the element, so it is +never collected, and jQuery UI walks the whole list of droppables on each move +of a drag. + +Use the jQuery `empty()` or `remove()` on any subtree that holds a widget, or +destroy each widget before you remove it. + +`clear()` in `dom.js` is the one place an island takes elements down, so it +calls the jQuery `empty()` when jQuery is there and falls back to `removeChild` +when it is not. An island that keeps its teardown in one function of its own +gets this right everywhere at once. + +The smoke test counts the entries in `$.ui.ddmanager` and in the jQuery data +cache across eight redraws and asks that neither one grows. Before the fix, the +pads and the sample rows of the SFZ builder leaked eighty droppables and +ninety-six data entries over those eight draws, and the panel draws again on +every click of a pad and every keystroke of the filter. + +### 4.12 Read what `main.less` already declares on every element + +`main.less` holds a second rule of the same shape as the one in 4.10: + + * { + font-family: "cooper hewitt", Sans-serif !important; + } + +A universal selector with `!important` is the strongest thing a stylesheet can +say about a property. +It is not beaten by a longer selector, by a later rule, or by the identifier +that every island rule starts with. +It also lands on each element of the panel one by one, so it beats inheritance +as well: the island named `var(--sfz-mono)` on `#sfzbuilder-library`, the rule +above named the page font on every child of it, and the child won. +The panel thus drew in the page font from the day it was written, and read +nothing at all from its two font tokens. +Nothing failed and nothing was logged; the header was where you saw it, as a +title in the wrong font, wide enough in that font to push the tool row onto a +line of its own. + +Two things follow. + +**Grep `main.less` for `!important` before you write a stylesheet.** +There are only a few such rules and they are the ones that will silently +overrule you. Today they cover `outline` and `font-family`. + +**A property you have to win, you win on every element, not on the panel.** + + #sfzbuilder-library, + #sfzbuilder-library * { + font-family: var(--sfz-mono) !important; + } + +The descendant half of the selector is what does the work. +A later rule of the island that names a class -- the status line asks for the +sans token -- still wins, because a class beats the `*`. +An element the island puts outside the panel needs the same treatment: the drag +helper of the SFZ builder lives on the body and carries a pair of rules of its +own. + +**Have a test read it.** +A rule that loses is invisible in the file, so the smoke test reads the computed +`font-family` of the title and of an element nested inside the panel and +compares each against the token, as 4.5 asks. +The nested element is the part that matters: the panel had the right family on +itself while everything inside it had the page font. + +## 5. Examples + +### 5.1 The seam + +The island supplies a `mount` function: + +```js +// html/js/app/sfzbuilder/mount.js +// @ts-check + +/** + * Puts the SFZ builder in an element. + * @param {HTMLElement} root An empty element. + * @returns {{ destroy: () => void }} + */ +export function mount(root) { /* ... */ } +``` + +The legacy code calls this function with a dynamic import: + +```js +// html/js/desktop.js (legacy) +JqueryClass('sfzBuilderBox', { + init: function () { + var self = $(this) + import('./app/sfzbuilder/mount.js').then(function (module) { + self.data('island', module.mount(self[0])) + }) + } +}) +``` + +A dynamic import operates in a classic script. +Thus `index.html` needs no new script element. + +### 5.2 The model and its test + +```js +// html/js/app/sfzbuilder/model.js +// @ts-check + +/** + * Changes the number of pads. Each pad keeps its index. + * @param {(Slot|null)[]} slots + * @param {number} count The new number of pads, 1 to 128. + * @returns {(Slot|null)[]} A new array. The input array does not change. + */ +export function resizePads(slots, count) { + const next = [] + for (let i = 0; i < count; i++) next.push(slots[i] ?? null) + return next +} +``` + +```js +// test/js/model.test.js +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { resizePads } from '../../html/js/app/sfzbuilder/model.js' + +test('resizePads keeps each pad at its index', () => { + const slots = [null, { sample: 'kick.wav' }, null] + assert.deepEqual(resizePads(slots, 2), [null, { sample: 'kick.wav' }]) +}) +``` + +This test needs no browser and no build step. +Run it with `node --test test/js/`. + +### 5.3 The tokens + +The tokens go on `:root`, so that they reach an element the island puts on the +`body`, and every name carries the prefix of the island. + +```css +:root { + --sfz-bg: #111; + --sfz-accent: #883996; + --sfz-focus: #e8e8e8; + --sfz-pad-min: 150px; + --sfz-gap: 8px; +} + +#sfzbuilder-library .sfz-pad-grid { + display: grid; + gap: var(--sfz-gap); + grid-template-columns: repeat(auto-fill, minmax(var(--sfz-pad-min), 1fr)); +} +``` + +Each rule starts at the identifier of the panel. `main.css` holds rules for the +bare elements -- `button`, `input`, `select`, `ul` -- and the identifier gives +the rules of the island the greater weight. + +### 5.4 The cache + +The script elements in `index.html` use a `?v={{version}}` parameter. +This parameter stops the browser from using an old file. +We keep this parameter now. We do not change the 41 script elements. + +An island is different. +The legacy code loads an island with a dynamic import, and the island makes its +own `` for its stylesheet. +Neither of these has a version parameter. + +We first accepted this risk. The risk became a fault on the device: + +* `TimelessStaticFileHandler` gives **every** static file `Cache-Control: + public, max-age=31536000` and no ETag. +* `{{version}}` is `IMAGE_VERSION`, which the server reads from + `/etc/mod-release/release`. That value does **not** change when you copy new + files onto a device. +* Thus the browser held the island files for one year -- and held the **404** + from a build that did not have those files yet. +* An island makes all of its own markup. A file that the browser cannot read + gives an empty panel and **no other sign**. + +`IslandStaticFileHandler` in `mod/webserver.py` now serves `html/js/app/` with +`Cache-Control: no-cache` and an ETag. The browser asks each time and the server +answers 304 when the file did not change. +The 41 script elements of `index.html` do not change. + +### 5.5 The checks + +The `Makefile` holds every check. Continuous integration calls the targets and +names no test file, so a new test runs without a change to either one. + +| Target | What it answers | +|---|---| +| `make compile` | Do the server files parse? | +| `make test-py` | Does the model on the server behave? `unittest discover` finds `test/test_*.py`. | +| `make test-js` | Does the model in the browser behave? `node --test` finds `test/js/**/*.test.js`, with no browser and no build step. | +| `make typecheck` | Do the JSDoc types hold? `tsc --noEmit` over `html/js/app/`. | +| `make smoke` | Does the panel draw, style, focus and drag in a real browser? | +| `make test` | All of the above. Run this before you push. | + +`test/test_setup_data_files.py` belongs to `make test-py` and is the check that +rule 4.9 asks for: it runs the globs of `setup.py` and asks that every file +under `html/js/app/` is in one of them. + +## 6. Tradeoffs + +**No bundler.** +The browser makes one request for each module file. +This is slow on the internet. It is fast on the local network. +We accept this cost. We do not accept the cost of a build step now. + +**JSDoc types, not TypeScript.** +JSDoc is longer than TypeScript syntax. +Some advanced types are difficult to write. +But JSDoc needs no build step. This is more important now. + +**Two code styles in one repository.** +New code and legacy code look different. +This can be a problem for a new person. +The directory `html/js/app/` makes the boundary clear. + +**The islands do not share code at first.** +The second island can find that it needs code from the first island. +Then you must move that code to a shared directory. +Do not make a shared directory before two islands need it. + +**The island files ask the server each time.** +An island file carries no version parameter, so `IslandStaticFileHandler` gives +it `Cache-Control: no-cache`. +The browser thus makes one small request for each island file on each visit. +The server answers 304 and sends no body. +On the local network this cost is small. It is smaller than the cost of a panel +that is empty because the browser holds an old file. + +**One test needs a browser.** +`make smoke` needs headless Chrome, so it is the one check that does not run +from Python and node alone. +It is worth the cost: it is the only place that can answer a question about the +computed style, the focus or a drag, and those are the faults that an island +makes, because an island draws everything itself. + +**The legacy problems continue.** +This design does not repair `pedalboard.js` or `modgui.js`. +It stops the growth of the problem. It does not remove the problem. + +## 7. The SFZ builder + +The SFZ builder is the first island. What was decided, and what it became: + +| Question | Decision | Outcome | +|---|---|---| +| Directory | Use `html/js/app//`. | `html/js/app/sfzbuilder/`: `mount.js`, `view.js`, `model.js`, `state.js`, `api.js`, `dom.js`, `legacy.js`, `sfzbuilder.css`. | +| Number of commits | Two. Commit 1 gives the structure. Commit 2 gives the new design. | Held. `docs/sfzbuilder-inventory.md` was the test for commit 1. | +| Continuous integration | Add a workflow. | `.github/workflows/ci.yml`, two jobs, calling the targets of section 5.5. | +| The `?v=` parameter | Do not change it. `html/js/app/` gets `no-cache` instead. | `IslandStaticFileHandler` in `mod/webserver.py`. See section 5.4. | +| Tokens | Write tokens for the SFZ builder only. | On `:root`, each name prefixed `--sfz-`. See rule 4.5. | + +Open question 1 of the earlier draft asked which steps the workflow runs in +commit 1. It runs all of them: `compile`, `test-py`, `test-js`, `typecheck` and +`smoke`. + +### What the first island taught + +Each of these is now a rule above, and each came from a fault in the panel. + +1. A file that the server does not send gives an empty panel and no other sign, + because the island draws everything. Hence the `setup.py` check (4.9), the + message on the seam, and the check that the stylesheet loaded. +2. The browser held those files, and their 404, for a year (5.4). +3. A token that does not reach the `body` makes a drag helper of black text on + nothing (4.5). +4. `* { outline: 0 !important }` in `main.less` takes the focus ring off the + panel, and several controls could not hold focus in any case (4.10). +5. A value that changes width moves the control under the pointer. A stepper + holds one width whatever it shows. +6. A test that names a colour breaks when the design changes and says nothing + when the code breaks (4.5). +7. An island draws its own elements, so it takes them down itself, and a native + `removeChild` leaks every jQuery UI widget on them (4.11). +8. `* { font-family: ... !important }` in `main.less` beat the font tokens on + every element of the panel, and the panel drew in the page font from the day + it was written (4.12). The same shape of rule as 4: read what `main.less` + already says about a property before you say it yourself. +9. Space between two rows of a header belongs in a `gap`, not in a margin on + the row above. The tool row of the SFZ builder is hidden while no bank is + open, and the margin under the title row was drawn all the same. +10. A path that arrives from the browser is not checked by anything before the + handler that uses it. `build_bank` copied from any absolute path whose name + ended in an audio extension, and it now takes one only from the places the + panel lists samples from. +11. A list the panel keeps a second copy of drifts. `model.js` holds the audio + extensions again, so the panel can drop a file the server would refuse + before it sends a batch; a test reads both files and asks that they agree. + Do this for the copy or delete the copy. Do not leave a comment asking the + next person to remember. + +### Open questions + +1. When do we add a bundler and TypeScript? +2. Which shared directory do two islands use? Do not make this directory before + two islands need it. +3. How does a long panel keep a short path for the keyboard? The SFZ builder + puts every pad and every control inside it in the tab order, which is correct + and slow at 128 pads. A roving `tabindex` with the arrow keys is the usual + answer, and it is not built (4.10). diff --git a/html/css/less/blokas.less b/html/css/less/blokas.less index 55d79ff9..e21ba3dd 100644 --- a/html/css/less/blokas.less +++ b/html/css/less/blokas.less @@ -1,19 +1,24 @@ .blokas { + header#pedalboard-actions, #bank-library header, #cloud-plugins-library header, #patchstorage-library header, #pedalboards-library header, - #file-manager-library header { + #file-manager-library header, + #sfzbuilder-library header { background-image: none !important; padding-left: 10px !important; } + #pedalboard-info { left: 10px !important; } + path.loadinglogo { display: none !important; } + #pedalboards-library .pedalboards, #bank-library #bank-edit, #file-manager-library, @@ -21,10 +26,12 @@ #cloud-plugins-library .cloud-plugins { background-image: none !important; } + #blokas-update { background-image: url(../img/icons/36/update.png); + &.uptodate { - background-image: url(../img/icons/36/ok.png); + background-image: url(../img/icons/36/ok.png); } } diff --git a/html/css/less/main.less b/html/css/less/main.less index fef7bf2a..3f038fdf 100644 --- a/html/css/less/main.less +++ b/html/css/less/main.less @@ -72,6 +72,7 @@ @import 'cloud_plugins.less'; @import 'patchstorage.less'; @import 'banks.less'; +@import 'sfzbuilder.less'; @import 'file_manager.less'; @import 'pedalboards.less'; @import 'nprogress.less'; @@ -366,6 +367,29 @@ hr.dotted { background-position: -180px bottom; } } + // pi-Stomp: the shared sprite has no free frame, so this icon is inline SVG. + // Two pads that hold a sample and two that are free. This says what the panel + // does. The four squares of the earlier icon looked the same as the + // Constructor icon. + // + // The sprite holds each other icon two times: faded for the state that is not + // active and full for hover and selected. One drawing and an opacity gives + // the same result here. + #mod-sfzbuilder { + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2245%22%20height%3D%2245%22%20viewBox%3D%220%200%2045%2045%22%3E%3Cg%20fill%3D%22%23F29446%22%3E%3Crect%20x%3D%2210%22%20y%3D%2210%22%20width%3D%2211%22%20height%3D%2211%22%2F%3E%3Crect%20x%3D%2224%22%20y%3D%2224%22%20width%3D%2211%22%20height%3D%2211%22%2F%3E%3C%2Fg%3E%3Cg%20fill%3D%22none%22%20stroke%3D%22%23ffffff%22%20stroke-width%3D%222%22%3E%3Crect%20x%3D%2225%22%20y%3D%2211%22%20width%3D%229%22%20height%3D%229%22%2F%3E%3Crect%20x%3D%2211%22%20y%3D%2225%22%20width%3D%229%22%20height%3D%229%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E"); + background-position: center; + background-repeat: no-repeat; + opacity: 0.33; + transition: all 0.33s; + &:hover { + background-color: #000; + opacity: 1; + } + &.selected { + background-color: #000; + opacity: 1; + } + } #banks-saving { margin: 21px 0 0 6px; font-size: 10px; @@ -559,7 +583,8 @@ header#pedalboard-actions, #cloud-plugins-library header, #patchstorage-library header, #pedalboards-library header, -#file-manager-library header { +#file-manager-library header, +#sfzbuilder-library header { display: block; left: 0; height: 45px; diff --git a/html/css/less/sfzbuilder.less b/html/css/less/sfzbuilder.less new file mode 100644 index 00000000..a799ce00 --- /dev/null +++ b/html/css/less/sfzbuilder.less @@ -0,0 +1,10 @@ +// pi-Stomp: SFZ Sound Bank Builder. +// +// This file is empty on purpose. +// +// The panel is an island. Its stylesheet is html/js/app/sfzbuilder/sfzbuilder.css +// and mount.js adds that file to the page. Thus the markup, the code and the +// style of the feature are in one directory, and a change to the panel does not +// make main.css change. +// +// See docs/frontend-architecture.md, rules 4.3 and 4.5. diff --git a/html/css/main.css b/html/css/main.css index 2a362b32..525a6028 100644 --- a/html/css/main.css +++ b/html/css/main.css @@ -1,9 +1 @@ -/*! 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:#D91E36;border-color:#c31b30}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#ac182b;border-color:#530b15}.btn-danger:hover{color:#fff;background-color:#ac182b;border-color:#8d1323}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#ac182b;background-image:none;border-color:#8d1323}.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:#8d1323;border-color:#530b15}.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:#D91E36;border-color:#c31b30}.btn-danger .badge{color:#D91E36;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:#D91E36}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#ac182b}.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:#D91E36}.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:#cccccc}.cloud-plugins h3{color:#cccccc}.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 #444444;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:#ffffff}.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}#patchstorage-library{background:#2c2c2c;bottom:45px;left:0;overflow:auto;position:fixed;right:0;top:0;z-index:2}#patchstorage-library h1{color:#fff;display:inline-block;font-size:24px;line-height:35px;margin:0 0 18px 0;padding:0}#patchstorage-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}#patchstorage-library header .close{opacity:1;position:absolute;right:10px;top:16px}#patchstorage-library .filter{bottom:45px;left:0;position:fixed;top:45px;right:0;width:240px;overflow:auto}#patchstorage-library .filter h1{font-weight:2em;text-transform:uppercase;font-weight:bold;margin:0 0 0 10px}#patchstorage-library .filter h2{color:white;font-size:15px;text-transform:uppercase;font-weight:bold;margin-top:1em;margin-left:10px}#patchstorage-library .filter .control-group .switch{margin:5px 0 5px 15px;position:relative;width:18rem;height:2.5rem;font-size:0}#patchstorage-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}#patchstorage-library .filter .control-group .switch input:checked{z-index:1}#patchstorage-library .filter .control-group .switch input:checked+label{opacity:1;cursor:default}#patchstorage-library .filter .control-group .switch input:checked~.toggle-outside .toggle-inside{left:.25rem;background:#883996}#patchstorage-library .filter .control-group .switch input:not(:checked)+label:hover{opacity:.5}#patchstorage-library .filter .control-group .switch input~input:checked~.toggle-outside .toggle-inside{left:2.65rem;background:#4a4a4a}#patchstorage-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}#patchstorage-library .filter .control-group .switch label:first-of-type{text-align:left}#patchstorage-library .filter .control-group .switch label:last-of-type{margin-left:5.5rem}#patchstorage-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}#patchstorage-library .filter .control-group .switch .toggle-inside{border-radius:5rem;background:#4a4a4a;position:absolute;transition:.25s ease all;height:2rem;width:2rem}#patchstorage-library .filter .control-group select{width:50px}#patchstorage-library .filter .control-group input[type='checkbox']{display:none}#patchstorage-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}#patchstorage-library .filter .control-group input[type='checkbox']+label:hover{background:#000}#patchstorage-library .filter .control-group input[type='checkbox']:checked+label{background:#F29446}#patchstorage-library .filter .control-group input[type='checkbox']:checked+label:hover{background:#600E6F}#patchstorage-library .filter>.form-group .input-clean{margin-right:8px !important}#patchstorage-library .filter .categories{width:100%;margin:0 0 15px 0}#patchstorage-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}#patchstorage-library .filter .categories li span{float:right}#patchstorage-library .filter .categories li.selected{background:#883996;color:#fff}#patchstorage-library .filter .categories li:hover{background:#000}#patchstorage-library .filter .categories li.selected:hover{background:#600E6F}.patchstorage-plugins{left:240px;position:fixed;right:0;top:45px;bottom:45px;background:#111 url(../img/watermark.png) 100% 100% no-repeat;overflow-y:auto}.patchstorage-plugins .plugins-wrapper{width:100%}.patchstorage-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){.patchstorage-plugins .cloud-plugin{float:left;width:100%}}@media (min-width:992px){.patchstorage-plugins .cloud-plugin{float:left;width:50%}}@media (min-width:1240px){.patchstorage-plugins .cloud-plugin{float:left;width:33.33333333%}}.patchstorage-plugins .cloud-plugin:hover{background-color:black}.patchstorage-plugins .cloud-plugin:hover .description{background:rgba(0,0,0,0.8)}.patchstorage-plugins .cloud-plugin:hover .description hr{border-color:#F29446}.patchstorage-plugins .cloud-plugin:hover figure{top:-5px}.patchstorage-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}.patchstorage-plugins .cloud-plugin figure img{height:auto;margin:0 auto 3px;width:auto;pointer-events:none;max-width:256px;max-height:64px}.patchstorage-plugins .cloud-plugin .cloud-plugin-border{border:1px solid #333;width:100%;height:100%}.patchstorage-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)}.patchstorage-plugins .cloud-plugin .description .no_description{color:rgba(255,255,255,0.25)}.patchstorage-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)}.patchstorage-plugins .cloud-plugin .description .title{display:block;text-transform:uppercase;font-weight:bold;padding:8px 10px 1px 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.patchstorage-plugins .cloud-plugin .description .author{display:block;padding:1px 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-transform:uppercase}.patchstorage-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}.patchstorage-plugins .cloud-plugin .actions{font-size:11px;font-weight:bold;text-transform:uppercase}.patchstorage-plugins .cloud-plugin .actions>span{cursor:pointer}.patchstorage-plugins .cloud-plugin .actions>span:hover{text-decoration:underline}.patchstorage-plugins .cloud-plugin .actions span.duplicate{margin:0 6px}.patchstorage-plugins h2{margin-left:23px;font-size:20px;color:#cccccc}.patchstorage-plugins h3{color:#cccccc}.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.downloads{border:1px solid black;background-color:black}.plugin-container .status.state.ready-to-go{display:none;border:1px solid #8AC926;background-color:#8AC926}.plugin-container .status.state.ready-to-go:after{content:"ready"}.plugin-container .status.state.help-needed{border:1px solid #D91E36;background-color:#D91E36}.plugin-container .status.state.help-needed:after{content:"help"}.plugin-container .status.state.inactive{border:1px solid #D91E36;background-color:#D91E36}.plugin-container .status.state.inactive:after{content:"deprecated"}.plugin-container .status.state.work-in-progress{border:1px solid #1982C4;background-color:#1982C4}.plugin-container .status.state.work-in-progress:after{content:"work in progress"}.plugin-container .status.installed{border:1px solid #883996;background-color:#883996}.plugin-container .status.installed:after{content:"installed"}.plugin-container .status.unavailable{border:1px solid #D91E36;background-color:#D91E36}.plugin-container .status.unavailable:after{content:"removed from patchstorage"}.plugin-container .status.local{border:1px solid #D91E36;background-color:#D91E36}.plugin-container .status.local:after{content:"local"}.plugin-container .status.outdated{border:1px solid #F29446;background-color:#F29446;color:#212121}.plugin-container .status.outdated:after{content:"new version"}.plugin-container .status.buildEnvironment{border:1px solid #D91E36;background-color:#D91E36}.plugin-container .status.buildEnvironment:after{content:"local"}.plugin-container .status.buildEnvironment.dev:after{content:"dev"}.plugin-container .status.buildEnvironment.prod{display:none}.plugin-container .status.buildEnvironment.prod:after{content:""}.patchstorage-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}.patchstorage-info .box{margin-top:3em;margin-bottom:3em;background-color:#111;padding:15px;border:2px solid #333}.patchstorage-info .box .row-fluid{position:relative}.patchstorage-info .close{color:#fff;font-size:12px;font-weight:bold;opacity:1;position:absolute;right:10px;text-shadow:none;text-transform:uppercase;top:10px}.patchstorage-info .screenshot{padding:18px;min-width:3em;min-height:9em;display:inline-block;z-index:0}.patchstorage-info .screenshot div.state{background-color:#FF2927;font-family:"cooper hewitt",sans-serif;text-transform:uppercase;font-weight:700;padding:5px 10px;color:white;position:absolute;top:18px;left:30px;font-size:16px}.patchstorage-info .screenshot div.state.ready-to-go{display:none;background-color:#8AC926}.patchstorage-info .screenshot div.state.ready-to-go:after{content:"state - ready to go!"}.patchstorage-info .screenshot div.state.help-needed{background-color:#D91E36}.patchstorage-info .screenshot div.state.help-needed:after{content:"state - help needed"}.patchstorage-info .screenshot div.state.inactive{background-color:#D91E36}.patchstorage-info .screenshot div.state.inactive:after{content:"state - deprecated"}.patchstorage-info .screenshot div.state.work-in-progress{background-color:#1982C4}.patchstorage-info .screenshot div.state.work-in-progress:after{content:"state - work in progress"}.patchstorage-info .screenshot img{max-width:100%}.patchstorage-info figure{position:relative;min-height:1px;padding-right:15px;padding-left:15px}@media (min-width:992px){.patchstorage-info figure{float:left;width:33.33333333%}}.patchstorage-info figure:hover{z-index:10 !important}.patchstorage-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){.patchstorage-info .plugin-description{float:left;width:58.33333333%}}.patchstorage-info .plugin-description>p{font-size:16px;margin-bottom:27px;white-space:pre-wrap}.patchstorage-info header{padding:18px 0 0}.patchstorage-info header h1{display:inline-block;vertical-align:middle;color:#fff;font-size:27px;font-weight:bold;line-height:1;margin:0;padding:0}.patchstorage-info header h1 span{font-size:16px;line-height:1;margin:0;padding:0}.patchstorage-info .ps-label{padding:1px 4px;text-transform:uppercase;font-weight:bold;font-size:9px;border:1px solid #03a9f4;background:#03a9f4;color:white;vertical-align:text-top;display:none}.patchstorage-info .version{color:#ccc;font-size:12px;font-weight:bold;text-transform:uppercase}.patchstorage-info .version span.bold{color:#fff;font-size:14px}.patchstorage-info .version button{font-weight:bold;margin-top:6px;text-transform:uppercase}.patchstorage-info .rating{background:url(../img/icn-rating.png) no-repeat -12px top;display:inline-block;height:14px;width:84px}.patchstorage-info .rating.one{background:url(../img/icn-rating.png) no-repeat -12px -15px}.patchstorage-info .rating.two{background:url(../img/icn-rating.png) no-repeat -12px -30px}.patchstorage-info .rating.three{background:url(../img/icn-rating.png) no-repeat -12px -45px}.patchstorage-info .rating.four{background:url(../img/icn-rating.png) no-repeat -12px -60px}.patchstorage-info .rating.five{background:url(../img/icn-rating.png) no-repeat -12px -75px}.patchstorage-info .rating-label{color:#fff;font-size:12px;font-weight:bold;text-transform:uppercase}.patchstorage-info .my-rate{width:15px;height:15px;margin:0;padding:0;float:left;cursor:pointer}.patchstorage-info .plugin-specs{text-align:center;width:100%;border-color:#333}.patchstorage-info .plugin-specs th{background:#333;color:#fff;border-color:#333;text-align:center}.patchstorage-info .plugin-specs td{border-color:#333}.patchstorage-info .plugin-specs td:first-child{width:40%}.patchstorage-info .plugin-specs td:not(first-child){width:20%}.patchstorage-info .comments-reviews{border-top:1px solid #333;margin-top:10px;padding-top:10px}.patchstorage-info .comments-reviews h1{color:#fff;font-size:21px;line-height:2;margin:0;padding:0}.patchstorage-info .reviews{border-bottom:1px solid #333;margin:9px 0 18px 0;padding:9px 0 18px 0}.patchstorage-info article.review{margin:18px 0}.patchstorage-info article.review h1{color:#fff;font-size:14px}.patchstorage-info article.review h1 .rating{margin-left:6px}.patchstorage-info article.review h1 .rating-label{color:#ccc;display:inline;font-size:12px;font-weight:normal;margin-left:6px}.patchstorage-info article.review .bar{background:#333;display:block;height:12px;margin:2px 6px 0;position:relative;width:100px}.patchstorage-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%}.patchstorage-info article.review .rating-number{display:block;font-size:12px;margin-top:-1px}.patchstorage-info article.comment{border-top:1px solid #333;margin:18px 0 0;padding:18px 0 0}.patchstorage-info article.comment h1{color:#fff;font-size:16px;line-height:1}.patchstorage-info article.comment h1 .date{color:#ccc;font-size:12px;font-weight:normal;margin-left:6px}.patchstorage-info .comments{margin:18px 0;padding:18px 0}.patchstorage-info .comments button{margin-top:18px}.patchstorage-info form.comment-form{border-top:1px solid #333;margin:18px 0;padding:18px 0}.patchstorage-info form.comment-form label{font-size:12px;font-weight:bold;text-transform:uppercase}.patchstorage-info form.comment-form textarea{height:180px}.patchstorage-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;opacity:.5}.patchstorage-info .favorite-button.favorite{opacity:1;background:url(../img/icons/25/star.png) no-repeat center center}.patchstorage-info .favorite-button.favorite:hover{opacity:1}.patchstorage-info .online-button-href{display:inline-block;margin-bottom:10px}.patchstorage-info .status.demo{display:inline-block;padding:4px 8px;margin:2px 0;font-size:.8em}#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:#BBBBBB;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 #patchstorage{background-image:url(../img/patchstorage-menu.png?v=2);background-position:0 top;background-repeat:no-repeat;transition:all .33s;background-size:cover}#main-menu #patchstorage:hover{background-color:#000;background-position:0 bottom}#main-menu #patchstorage.selected{background-color:#000;background-position:0 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:#ffffff;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,#patchstorage_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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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,#patchstorage-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:#222222;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 #444444;height:20px;width:330px}#mod-devices-window .progressbar,#mod-upgrade .progressbar{background-color:#8888ff;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 .screenshot img{max-width:100%}.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 #CCCCCC}.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}.blokas header#pedalboard-actions,.blokas #bank-library header,.blokas #cloud-plugins-library header,.blokas #patchstorage-library header,.blokas #pedalboards-library header,.blokas #file-manager-library header{background-image:none !important;padding-left:10px !important}.blokas #pedalboard-info{left:10px !important}.blokas path.loadinglogo{display:none !important}.blokas #pedalboards-library .pedalboards,.blokas #bank-library #bank-edit,.blokas #file-manager-library,.blokas #patchstorage-library .patchstorage-plugins,.blokas #cloud-plugins-library .cloud-plugins{background-image:none !important}.blokas #blokas-update{background-image:url(../img/icons/36/update.png)}.blokas #blokas-update.uptodate{background-image:url(../img/icons/36/ok.png)} -/* Session recording toolbar button (see mod/recorder.py). Self-contained: #main-menu's - shared icon rule lives inside the giant minified block above and isn't safe to hand-edit, - so this repeats the layout properties it would otherwise inherit (float/padding/color/etc). */ -#main-menu #mod-recorder{position:relative;float:right;padding-left:40px;padding-right:12px;color:#fff;font-size:11px;font-weight:bold;line-height:4.5;text-transform:uppercase;width:115px !important;transition:all 0.33s;} -#main-menu #mod-recorder:hover{background-color:#000;} -#main-menu #mod-recorder:before{content:"";position:absolute;left:15px;top:50%;margin-top:-6px;width:12px;height:12px;border-radius:50%;background-color:#999;transition:background-color 0.33s;} -#main-menu #mod-recorder.recording:before{background-color:#ff3030;animation:mod-recorder-pulse 1.5s ease-in-out infinite;} -@keyframes mod-recorder-pulse{0%{opacity:1;}50%{opacity:0.35;}100%{opacity:1;}} +/*! 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:#D91E36;border-color:#c31b30}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#ac182b;border-color:#530b15}.btn-danger:hover{color:#fff;background-color:#ac182b;border-color:#8d1323}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#ac182b;background-image:none;border-color:#8d1323}.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:#8d1323;border-color:#530b15}.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:#D91E36;border-color:#c31b30}.btn-danger .badge{color:#D91E36;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:#D91E36}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#ac182b}.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:#D91E36}.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:#cccccc}.cloud-plugins h3{color:#cccccc}.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 #444444;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:#ffffff}.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}#patchstorage-library{background:#2c2c2c;bottom:45px;left:0;overflow:auto;position:fixed;right:0;top:0;z-index:2}#patchstorage-library h1{color:#fff;display:inline-block;font-size:24px;line-height:35px;margin:0 0 18px 0;padding:0}#patchstorage-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}#patchstorage-library header .close{opacity:1;position:absolute;right:10px;top:16px}#patchstorage-library .filter{bottom:45px;left:0;position:fixed;top:45px;right:0;width:240px;overflow:auto}#patchstorage-library .filter h1{font-weight:2em;text-transform:uppercase;font-weight:bold;margin:0 0 0 10px}#patchstorage-library .filter h2{color:white;font-size:15px;text-transform:uppercase;font-weight:bold;margin-top:1em;margin-left:10px}#patchstorage-library .filter .control-group .switch{margin:5px 0 5px 15px;position:relative;width:18rem;height:2.5rem;font-size:0}#patchstorage-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}#patchstorage-library .filter .control-group .switch input:checked{z-index:1}#patchstorage-library .filter .control-group .switch input:checked+label{opacity:1;cursor:default}#patchstorage-library .filter .control-group .switch input:checked~.toggle-outside .toggle-inside{left:.25rem;background:#883996}#patchstorage-library .filter .control-group .switch input:not(:checked)+label:hover{opacity:.5}#patchstorage-library .filter .control-group .switch input~input:checked~.toggle-outside .toggle-inside{left:2.65rem;background:#4a4a4a}#patchstorage-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}#patchstorage-library .filter .control-group .switch label:first-of-type{text-align:left}#patchstorage-library .filter .control-group .switch label:last-of-type{margin-left:5.5rem}#patchstorage-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}#patchstorage-library .filter .control-group .switch .toggle-inside{border-radius:5rem;background:#4a4a4a;position:absolute;transition:.25s ease all;height:2rem;width:2rem}#patchstorage-library .filter .control-group select{width:50px}#patchstorage-library .filter .control-group input[type='checkbox']{display:none}#patchstorage-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}#patchstorage-library .filter .control-group input[type='checkbox']+label:hover{background:#000}#patchstorage-library .filter .control-group input[type='checkbox']:checked+label{background:#F29446}#patchstorage-library .filter .control-group input[type='checkbox']:checked+label:hover{background:#600E6F}#patchstorage-library .filter>.form-group .input-clean{margin-right:8px !important}#patchstorage-library .filter .categories{width:100%;margin:0 0 15px 0}#patchstorage-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}#patchstorage-library .filter .categories li span{float:right}#patchstorage-library .filter .categories li.selected{background:#883996;color:#fff}#patchstorage-library .filter .categories li:hover{background:#000}#patchstorage-library .filter .categories li.selected:hover{background:#600E6F}.patchstorage-plugins{left:240px;position:fixed;right:0;top:45px;bottom:45px;background:#111 url(../img/watermark.png) 100% 100% no-repeat;overflow-y:auto}.patchstorage-plugins .plugins-wrapper{width:100%}.patchstorage-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){.patchstorage-plugins .cloud-plugin{float:left;width:100%}}@media (min-width:992px){.patchstorage-plugins .cloud-plugin{float:left;width:50%}}@media (min-width:1240px){.patchstorage-plugins .cloud-plugin{float:left;width:33.33333333%}}.patchstorage-plugins .cloud-plugin:hover{background-color:black}.patchstorage-plugins .cloud-plugin:hover .description{background:rgba(0,0,0,0.8)}.patchstorage-plugins .cloud-plugin:hover .description hr{border-color:#F29446}.patchstorage-plugins .cloud-plugin:hover figure{top:-5px}.patchstorage-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}.patchstorage-plugins .cloud-plugin figure img{height:auto;margin:0 auto 3px;width:auto;pointer-events:none;max-width:256px;max-height:64px}.patchstorage-plugins .cloud-plugin .cloud-plugin-border{border:1px solid #333;width:100%;height:100%}.patchstorage-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)}.patchstorage-plugins .cloud-plugin .description .no_description{color:rgba(255,255,255,0.25)}.patchstorage-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)}.patchstorage-plugins .cloud-plugin .description .title{display:block;text-transform:uppercase;font-weight:bold;padding:8px 10px 1px 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.patchstorage-plugins .cloud-plugin .description .author{display:block;padding:1px 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-transform:uppercase}.patchstorage-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}.patchstorage-plugins .cloud-plugin .actions{font-size:11px;font-weight:bold;text-transform:uppercase}.patchstorage-plugins .cloud-plugin .actions>span{cursor:pointer}.patchstorage-plugins .cloud-plugin .actions>span:hover{text-decoration:underline}.patchstorage-plugins .cloud-plugin .actions span.duplicate{margin:0 6px}.patchstorage-plugins h2{margin-left:23px;font-size:20px;color:#cccccc}.patchstorage-plugins h3{color:#cccccc}.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.downloads{border:1px solid black;background-color:black}.plugin-container .status.state.ready-to-go{display:none;border:1px solid #8AC926;background-color:#8AC926}.plugin-container .status.state.ready-to-go:after{content:"ready"}.plugin-container .status.state.help-needed{border:1px solid #D91E36;background-color:#D91E36}.plugin-container .status.state.help-needed:after{content:"help"}.plugin-container .status.state.inactive{border:1px solid #D91E36;background-color:#D91E36}.plugin-container .status.state.inactive:after{content:"deprecated"}.plugin-container .status.state.work-in-progress{border:1px solid #1982C4;background-color:#1982C4}.plugin-container .status.state.work-in-progress:after{content:"work in progress"}.plugin-container .status.installed{border:1px solid #883996;background-color:#883996}.plugin-container .status.installed:after{content:"installed"}.plugin-container .status.unavailable{border:1px solid #D91E36;background-color:#D91E36}.plugin-container .status.unavailable:after{content:"removed from patchstorage"}.plugin-container .status.local{border:1px solid #D91E36;background-color:#D91E36}.plugin-container .status.local:after{content:"local"}.plugin-container .status.outdated{border:1px solid #F29446;background-color:#F29446;color:#212121}.plugin-container .status.outdated:after{content:"new version"}.plugin-container .status.buildEnvironment{border:1px solid #D91E36;background-color:#D91E36}.plugin-container .status.buildEnvironment:after{content:"local"}.plugin-container .status.buildEnvironment.dev:after{content:"dev"}.plugin-container .status.buildEnvironment.prod{display:none}.plugin-container .status.buildEnvironment.prod:after{content:""}.patchstorage-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}.patchstorage-info .box{margin-top:3em;margin-bottom:3em;background-color:#111;padding:15px;border:2px solid #333}.patchstorage-info .box .row-fluid{position:relative}.patchstorage-info .close{color:#fff;font-size:12px;font-weight:bold;opacity:1;position:absolute;right:10px;text-shadow:none;text-transform:uppercase;top:10px}.patchstorage-info .screenshot{padding:18px;min-width:3em;min-height:9em;display:inline-block;z-index:0}.patchstorage-info .screenshot div.state{background-color:#FF2927;font-family:"cooper hewitt",sans-serif;text-transform:uppercase;font-weight:700;padding:5px 10px;color:white;position:absolute;top:18px;left:30px;font-size:16px}.patchstorage-info .screenshot div.state.ready-to-go{display:none;background-color:#8AC926}.patchstorage-info .screenshot div.state.ready-to-go:after{content:"state - ready to go!"}.patchstorage-info .screenshot div.state.help-needed{background-color:#D91E36}.patchstorage-info .screenshot div.state.help-needed:after{content:"state - help needed"}.patchstorage-info .screenshot div.state.inactive{background-color:#D91E36}.patchstorage-info .screenshot div.state.inactive:after{content:"state - deprecated"}.patchstorage-info .screenshot div.state.work-in-progress{background-color:#1982C4}.patchstorage-info .screenshot div.state.work-in-progress:after{content:"state - work in progress"}.patchstorage-info .screenshot img{max-width:100%}.patchstorage-info figure{position:relative;min-height:1px;padding-right:15px;padding-left:15px}@media (min-width:992px){.patchstorage-info figure{float:left;width:33.33333333%}}.patchstorage-info figure:hover{z-index:10 !important}.patchstorage-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){.patchstorage-info .plugin-description{float:left;width:58.33333333%}}.patchstorage-info .plugin-description>p{font-size:16px;margin-bottom:27px;white-space:pre-wrap}.patchstorage-info header{padding:18px 0 0}.patchstorage-info header h1{display:inline-block;vertical-align:middle;color:#fff;font-size:27px;font-weight:bold;line-height:1;margin:0;padding:0}.patchstorage-info header h1 span{font-size:16px;line-height:1;margin:0;padding:0}.patchstorage-info .ps-label{padding:1px 4px;text-transform:uppercase;font-weight:bold;font-size:9px;border:1px solid #03a9f4;background:#03a9f4;color:white;vertical-align:text-top;display:none}.patchstorage-info .version{color:#ccc;font-size:12px;font-weight:bold;text-transform:uppercase}.patchstorage-info .version span.bold{color:#fff;font-size:14px}.patchstorage-info .version button{font-weight:bold;margin-top:6px;text-transform:uppercase}.patchstorage-info .rating{background:url(../img/icn-rating.png) no-repeat -12px top;display:inline-block;height:14px;width:84px}.patchstorage-info .rating.one{background:url(../img/icn-rating.png) no-repeat -12px -15px}.patchstorage-info .rating.two{background:url(../img/icn-rating.png) no-repeat -12px -30px}.patchstorage-info .rating.three{background:url(../img/icn-rating.png) no-repeat -12px -45px}.patchstorage-info .rating.four{background:url(../img/icn-rating.png) no-repeat -12px -60px}.patchstorage-info .rating.five{background:url(../img/icn-rating.png) no-repeat -12px -75px}.patchstorage-info .rating-label{color:#fff;font-size:12px;font-weight:bold;text-transform:uppercase}.patchstorage-info .my-rate{width:15px;height:15px;margin:0;padding:0;float:left;cursor:pointer}.patchstorage-info .plugin-specs{text-align:center;width:100%;border-color:#333}.patchstorage-info .plugin-specs th{background:#333;color:#fff;border-color:#333;text-align:center}.patchstorage-info .plugin-specs td{border-color:#333}.patchstorage-info .plugin-specs td:first-child{width:40%}.patchstorage-info .plugin-specs td:not(first-child){width:20%}.patchstorage-info .comments-reviews{border-top:1px solid #333;margin-top:10px;padding-top:10px}.patchstorage-info .comments-reviews h1{color:#fff;font-size:21px;line-height:2;margin:0;padding:0}.patchstorage-info .reviews{border-bottom:1px solid #333;margin:9px 0 18px 0;padding:9px 0 18px 0}.patchstorage-info article.review{margin:18px 0}.patchstorage-info article.review h1{color:#fff;font-size:14px}.patchstorage-info article.review h1 .rating{margin-left:6px}.patchstorage-info article.review h1 .rating-label{color:#ccc;display:inline;font-size:12px;font-weight:normal;margin-left:6px}.patchstorage-info article.review .bar{background:#333;display:block;height:12px;margin:2px 6px 0;position:relative;width:100px}.patchstorage-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%}.patchstorage-info article.review .rating-number{display:block;font-size:12px;margin-top:-1px}.patchstorage-info article.comment{border-top:1px solid #333;margin:18px 0 0;padding:18px 0 0}.patchstorage-info article.comment h1{color:#fff;font-size:16px;line-height:1}.patchstorage-info article.comment h1 .date{color:#ccc;font-size:12px;font-weight:normal;margin-left:6px}.patchstorage-info .comments{margin:18px 0;padding:18px 0}.patchstorage-info .comments button{margin-top:18px}.patchstorage-info form.comment-form{border-top:1px solid #333;margin:18px 0;padding:18px 0}.patchstorage-info form.comment-form label{font-size:12px;font-weight:bold;text-transform:uppercase}.patchstorage-info form.comment-form textarea{height:180px}.patchstorage-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;opacity:.5}.patchstorage-info .favorite-button.favorite{opacity:1;background:url(../img/icons/25/star.png) no-repeat center center}.patchstorage-info .favorite-button.favorite:hover{opacity:1}.patchstorage-info .online-button-href{display:inline-block;margin-bottom:10px}.patchstorage-info .status.demo{display:inline-block;padding:4px 8px;margin:2px 0;font-size:.8em}#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:#BBBBBB;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 #patchstorage{background-image:url(../img/patchstorage-menu.png?v=2);background-position:0 top;background-repeat:no-repeat;transition:all .33s;background-size:cover}#main-menu #patchstorage:hover{background-color:#000;background-position:0 bottom}#main-menu #patchstorage.selected{background-color:#000;background-position:0 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 #mod-sfzbuilder{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2245%22%20height%3D%2245%22%20viewBox%3D%220%200%2045%2045%22%3E%3Cg%20fill%3D%22%23F29446%22%3E%3Crect%20x%3D%2210%22%20y%3D%2210%22%20width%3D%2211%22%20height%3D%2211%22%2F%3E%3Crect%20x%3D%2224%22%20y%3D%2224%22%20width%3D%2211%22%20height%3D%2211%22%2F%3E%3C%2Fg%3E%3Cg%20fill%3D%22none%22%20stroke%3D%22%23ffffff%22%20stroke-width%3D%222%22%3E%3Crect%20x%3D%2225%22%20y%3D%2211%22%20width%3D%229%22%20height%3D%229%22%2F%3E%3Crect%20x%3D%2211%22%20y%3D%2225%22%20width%3D%229%22%20height%3D%229%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");background-position:center;background-repeat:no-repeat;opacity:.33;transition:all .33s}#main-menu #mod-sfzbuilder:hover{background-color:#000;opacity:1}#main-menu #mod-sfzbuilder.selected{background-color:#000;opacity:1}#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,#main-menu #mod-recorder{background-position:8px 50%;background-repeat:no-repeat;padding-left:40px;padding-right:12px;color:#ffffff;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,#main-menu #mod-recorder: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,#main-menu #mod-recorder.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 #mod-recorder{position:relative;width:115px !important}#main-menu #mod-recorder:before{content:"";position:absolute;left:15px;top:50%;margin-top:-6px;width:12px;height:12px;border-radius:50%;background-color:#999;transition:background-color .33s}#main-menu #mod-recorder.recording:before{background-color:#ff3030;animation:mod-recorder-pulse 1.5s ease-in-out infinite}@keyframes mod-recorder-pulse{0%{opacity:1}50%{opacity:.35}100%{opacity:1}}#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,#patchstorage_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,#patchstorage-library header,#pedalboards-library header,#file-manager-library header,#sfzbuilder-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,#patchstorage-library header .help,#pedalboards-library header .help,#file-manager-library header .help,#sfzbuilder-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,#patchstorage-library header .close,#pedalboards-library header .close,#file-manager-library header .close,#sfzbuilder-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,#patchstorage-library header .drop-menu,#pedalboards-library header .drop-menu,#file-manager-library header .drop-menu,#sfzbuilder-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,#patchstorage-library header .drop-menu:hover,#pedalboards-library header .drop-menu:hover,#file-manager-library header .drop-menu:hover,#sfzbuilder-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,#patchstorage-library header .drop-menu li,#pedalboards-library header .drop-menu li,#file-manager-library header .drop-menu li,#sfzbuilder-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,#patchstorage-library header .drop-menu li:hover,#pedalboards-library header .drop-menu li:hover,#file-manager-library header .drop-menu li:hover,#sfzbuilder-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,#patchstorage-library header .drop-menu .sub-menu,#pedalboards-library header .drop-menu .sub-menu,#file-manager-library header .drop-menu .sub-menu,#sfzbuilder-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,#patchstorage-library header .drop-menu .sub-menu:hover,#pedalboards-library header .drop-menu .sub-menu:hover,#file-manager-library header .drop-menu .sub-menu:hover,#sfzbuilder-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,#patchstorage-library header .menu-trigger:hover+.drop-menu,#pedalboards-library header .menu-trigger:hover+.drop-menu,#file-manager-library header .menu-trigger:hover+.drop-menu,#sfzbuilder-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,#patchstorage-library header .sub-menu-trigger:hover ul,#pedalboards-library header .sub-menu-trigger:hover ul,#file-manager-library header .sub-menu-trigger:hover ul,#sfzbuilder-library header .sub-menu-trigger:hover ul{display:block}header#pedalboard-actions button,#bank-library header button,#cloud-plugins-library header button,#patchstorage-library header button,#pedalboards-library header button,#file-manager-library header button,#sfzbuilder-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,#patchstorage-library header button:hover,#pedalboards-library header button:hover,#file-manager-library header button:hover,#sfzbuilder-library header button:hover,header#pedalboard-actions button.selected,#bank-library header button.selected,#cloud-plugins-library header button.selected,#patchstorage-library header button.selected,#pedalboards-library header button.selected,#file-manager-library header button.selected,#sfzbuilder-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,#patchstorage-library header button.js-save,#pedalboards-library header button.js-save,#file-manager-library header button.js-save,#sfzbuilder-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,#patchstorage-library header button.js-save-as,#pedalboards-library header button.js-save-as,#file-manager-library header button.js-save-as,#sfzbuilder-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,#patchstorage-library header button.js-preset,#pedalboards-library header button.js-preset,#file-manager-library header button.js-preset,#sfzbuilder-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,#patchstorage-library header button#js-preset-enabler:hover,#pedalboards-library header button#js-preset-enabler:hover,#file-manager-library header button#js-preset-enabler:hover,#sfzbuilder-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,#patchstorage-library header button#js-preset-enabler.selected,#pedalboards-library header button#js-preset-enabler.selected,#file-manager-library header button#js-preset-enabler.selected,#sfzbuilder-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,#patchstorage-library header button.js-reset,#pedalboards-library header button.js-reset,#file-manager-library header button.js-reset,#sfzbuilder-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,#patchstorage-library header button.js-reset:hover,#pedalboards-library header button.js-reset:hover,#file-manager-library header button.js-reset:hover,#sfzbuilder-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,#patchstorage-library header button.js-reset.selected,#pedalboards-library header button.js-reset.selected,#file-manager-library header button.js-reset.selected,#sfzbuilder-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,#patchstorage-library header button.js-cv-addressing,#pedalboards-library header button.js-cv-addressing,#file-manager-library header button.js-cv-addressing,#sfzbuilder-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,#patchstorage-library header button.js-cv-addressing:hover,#pedalboards-library header button.js-cv-addressing:hover,#file-manager-library header button.js-cv-addressing:hover,#sfzbuilder-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,#patchstorage-library header button.js-cv-addressing.selected,#pedalboards-library header button.js-cv-addressing.selected,#file-manager-library header button.js-cv-addressing.selected,#sfzbuilder-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,#patchstorage-library header button.js-cloud,#pedalboards-library header button.js-cloud,#file-manager-library header button.js-cloud,#sfzbuilder-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,#patchstorage-library header h1,#pedalboards-library header h1,#file-manager-library header h1,#sfzbuilder-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:#222222;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 #444444;height:20px;width:330px}#mod-devices-window .progressbar,#mod-upgrade .progressbar{background-color:#8888ff;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 .screenshot img{max-width:100%}.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 #CCCCCC}.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}.blokas header#pedalboard-actions,.blokas #bank-library header,.blokas #cloud-plugins-library header,.blokas #patchstorage-library header,.blokas #pedalboards-library header,.blokas #file-manager-library header,.blokas #sfzbuilder-library header{background-image:none !important;padding-left:10px !important}.blokas #pedalboard-info{left:10px !important}.blokas path.loadinglogo{display:none !important}.blokas #pedalboards-library .pedalboards,.blokas #bank-library #bank-edit,.blokas #file-manager-library,.blokas #patchstorage-library .patchstorage-plugins,.blokas #cloud-plugins-library .cloud-plugins{background-image:none !important}.blokas #blokas-update{background-image:url(../img/icons/36/update.png)}.blokas #blokas-update.uptodate{background-image:url(../img/icons/36/ok.png)} \ No newline at end of file diff --git a/html/index.html b/html/index.html index d27b0f43..3e5c4c4b 100644 --- a/html/index.html +++ b/html/index.html @@ -146,6 +146,8 @@ bankList: $('#bank-list'), fileManagerBox: $('#file-manager-library'), fileManagerBoxTrigger: $('#main-menu #mod-file-manager'), + sfzBuilderBox: $('#sfzbuilder-library'), + sfzBuilderBoxTrigger: $('#main-menu #mod-sfzbuilder'), shareButton: $('#pedalboard-info .js-cloud'), shareWindow: $('#share-window'), presetSaveBox: $('#save-preset'), @@ -371,6 +373,7 @@ + @@ -730,6 +733,7 @@

Control Chain Device Update

+
Auto saving banks...
@@ -1117,6 +1121,13 @@

File Manager

+ + +
+ +
diff --git a/html/js/app/package.json b/html/js/app/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/html/js/app/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/html/js/app/sfzbuilder/api.js b/html/js/app/sfzbuilder/api.js new file mode 100644 index 00000000..1289299f --- /dev/null +++ b/html/js/app/sfzbuilder/api.js @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later +// @ts-check + +/** + * The network calls of the SFZ builder. + * This file knows all the URLs. No other file in the island knows them. + * @module + */ + +import { jq } from './legacy.js' + +/** + * @param {string} url + * @param {object} [data] + * @returns {Promise} + */ +function get(url, data) { + return Promise.resolve(jq().ajax({ + url: url, + data: data || {}, + dataType: 'json', + cache: false, + })) +} + +/** + * Gives the names of the banks and how many samples each one holds. + * @returns {Promise<{ banks: string[], counts: Record }>} + */ +export function listBanks() { + return get('/sfzbuilder/banks').then((data) => ({ + banks: data.banks || [], + counts: data.counts || {}, + })) +} + +/** + * Gives the base note and the pads of one bank. + * @param {string} name + * @returns {Promise} + */ +export function loadBank(name) { + return get('/sfzbuilder/bank', { name: name }) +} + +/** + * Makes a bank directory. + * @param {string} name + * @returns {Promise} + */ +export function createBank(name) { + return Promise.resolve(jq().ajax({ + url: '/sfzbuilder/bank', + type: 'POST', + data: JSON.stringify({ name: name }), + dataType: 'json', + cache: false, + })) +} + +/** + * Changes the name of a bank. + * + * The server moves the directory and renames the SFZ file inside it, because + * that file carries the name of its bank. The name it gives back is the one + * that reached the disk, which is not always the one that was asked for: a + * space becomes an underscore. + * @param {string} name + * @param {string} newName + * @returns {Promise} + */ +export function renameBank(name, newName) { + return Promise.resolve(jq().ajax({ + url: '/sfzbuilder/bank/rename', + type: 'POST', + data: JSON.stringify({ name: name, new_name: newName }), + dataType: 'json', + cache: false, + })) +} + +/** + * Removes a bank and everything in it. + * @param {string} name + * @returns {Promise} + */ +export function deleteBank(name) { + return Promise.resolve(jq().ajax({ + url: '/sfzbuilder/bank/delete', + type: 'POST', + data: JSON.stringify({ name: name }), + dataType: 'json', + cache: false, + })) +} + +/** + * Gives the audio files of one sample source. + * @param {string} source One of "bank", "device" or "usb". + * @param {string} bank The name of the current bank. + * @returns {Promise} + */ +export function listSamples(source, bank) { + if (source === 'bank') { + return get('/sfzbuilder/samples', { bank: bank }) + } + if (source === 'device') { + return get('/sfzbuilder/device', { exclude_bank: bank }) + } + return get('/sfzbuilder/usb') +} + +/** + * Sends audio files to a bank. + * @param {string} bank + * @param {File[]|FileList} files + * @returns {Promise} + */ +export function uploadSamples(bank, files) { + const form = new FormData() + form.append('bank', bank) + for (let i = 0; i < files.length; i++) { + form.append('file', files[i]) + } + return Promise.resolve(jq().ajax({ + url: '/sfzbuilder/upload', + type: 'POST', + data: form, + processData: false, + contentType: false, + dataType: 'json', + })) +} + +/** + * Gives the URL that plays one audio file. + * @param {string} fullname + * @returns {string} + */ +export function audioUrl(fullname) { + return '/sfzbuilder/audio/' + encodeURIComponent(fullname) +} + +/** + * Writes the SFZ file of a bank. + * @param {string} name + * @param {number} baseNote + * @param {(object|null)[]} slots + * @returns {Promise} + */ +export function buildBank(name, baseNote, slots) { + return Promise.resolve(jq().ajax({ + url: '/sfzbuilder/build', + type: 'POST', + data: JSON.stringify({ name: name, base_note: baseNote, slots: slots }), + dataType: 'json', + cache: false, + })) +} diff --git a/html/js/app/sfzbuilder/dom.js b/html/js/app/sfzbuilder/dom.js new file mode 100644 index 00000000..b78e5832 --- /dev/null +++ b/html/js/app/sfzbuilder/dom.js @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later +// @ts-check + +/** + * A small set of tools that make elements. + * + * The island makes its own markup (architecture rule 4.3). These functions use + * the browser API, and `clear` also uses jQuery to take an element down. They + * exist so that the view code stays short, not to replace a library. + * @module + */ + +import { jq } from './legacy.js' + +/** + * Makes an element. + * + * The tag can hold classes after a dot. Example: `h('div.sfz-pad.sfz-pad--on')`. + * A property that starts with "on" becomes an event listener. + * A child that is a string becomes a text node, thus the text is always safe. + * + * @param {string} tag + * @param {object} [props] + * @param {(Node|string|null|false|undefined)[]} [children] + * @returns {HTMLElement} + */ +export function h(tag, props, children) { + const parts = tag.split('.') + const node = document.createElement(parts[0] || 'div') + for (let i = 1; i < parts.length; i++) { + node.classList.add(parts[i]) + } + for (const key in props || {}) { + const value = (props || {})[key] + if (value === null || value === undefined || value === false) { + continue + } + if (key.indexOf('on') === 0) { + node.addEventListener(key.slice(2), value) + } else if (key === 'text') { + node.textContent = String(value) + } else if (key === 'html') { + node.innerHTML = String(value) + } else if (key in node && key !== 'list' && key !== 'form') { + // @ts-ignore - a direct property keeps the type, for example checked. + node[key] = value + } else { + node.setAttribute(key, String(value)) + } + } + for (const child of children || []) { + if (child === null || child === undefined || child === false) { + continue + } + node.appendChild(typeof child === 'string' ? document.createTextNode(child) : child) + } + return node +} + +/** + * Removes every child of an element. + * + * This uses the jQuery `empty`, not `removeChild`, whenever jQuery is there + * (architecture rule 4.11). jQuery UI keeps a `draggable` and a `droppable` in + * `$.ui.ddmanager` and in the jQuery data cache, and it takes either one out + * only through the `cleanData` hook of jQuery. A native `removeChild` never + * runs that hook, so the entries stayed after the element was gone: they held + * the element, which was thus never collected, and jQuery UI walks the whole + * list of droppables on every move of a drag. The panel draws the pads again on + * every click of a pad and the sample rows on every keystroke of the filter, so + * the two lists grew for as long as the panel was open. + * + * `empty` cleans the descendants of the element and not the element itself, + * which is what this function wants: the element stays and its children go. + * + * The plain path below is the one to take when jQuery is absent, so that this + * file can be read outside the browser. + * + * @param {HTMLElement} node + */ +export function clear(node) { + const $ = jq() + if ($) { + $(node).empty() + return + } + while (node.firstChild) { + node.removeChild(node.firstChild) + } +} + +/** + * Adds or removes a class. + * @param {Element|null} node + * @param {string} name + * @param {boolean} on + */ +export function toggleClass(node, name, on) { + if (node) { + node.classList.toggle(name, !!on) + } +} + +/** + * Shows or hides an element. + * @param {HTMLElement|null} node + * @param {boolean} on + */ +export function show(node, on) { + if (node) { + node.style.display = on ? '' : 'none' + } +} + +/** + * Makes an element that is not a button act as one. + * + * The bank rows, the sample rows and the pads are a div or a li, because each + * one holds its own buttons and a button cannot hold a button. An element like + * that takes a click but the keyboard cannot reach it, so it can carry no focus + * ring either. This function gives it a place in the tab order, a name that a + * screen reader reads, and the two keys that activate a button. + * + * The keys act only when the element itself has the focus. A press on a button + * inside it, for example the play control of a sample row, sends a click of its + * own and the keydown goes up to this element as well; without the test the row + * would act two times. + * + * The role is given by the caller, because it is not always "button". The + * children of a button are presentational: a screen reader is free to flatten + * them away and read the button as one piece of text. That is right for an + * element that holds text only, and wrong for one that holds a control of its + * own, whose control would then be lost. Pass a role that keeps its children, + * such as "group", or pass null to keep the role the element already has, which + * for a li inside a ul is "listitem". + * + * @param {HTMLElement} node + * @param {(event: Event) => void} onActivate + * @param {string|null} [role] The ARIA role, "button" when not given. + * @returns {HTMLElement} The same element. + */ +export function clickable(node, onActivate, role) { + const name = role === undefined ? 'button' : role + node.setAttribute('tabindex', '0') + if (name) { + node.setAttribute('role', name) + } + node.addEventListener('click', onActivate) + node.addEventListener('keydown', (event) => { + if (event.target !== node) { + return + } + if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') { + // A space would else scroll the list that holds the element. + event.preventDefault() + onActivate(event) + } + }) + return node +} diff --git a/html/js/app/sfzbuilder/legacy.js b/html/js/app/sfzbuilder/legacy.js new file mode 100644 index 00000000..fc2134a4 --- /dev/null +++ b/html/js/app/sfzbuilder/legacy.js @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later +// @ts-check + +/** + * The adapter to the legacy code. + * This is the only file in the island that reads a global name. + * @module + */ + +/** + * Gives the jQuery function from the legacy code. + * jQuery UI adds `draggable` and `droppable` to it. + * @returns {any} + */ +export function jq() { + // @ts-ignore - jQuery is a global from a classic script. + return window.jQuery +} diff --git a/html/js/app/sfzbuilder/model.js b/html/js/app/sfzbuilder/model.js new file mode 100644 index 00000000..23fba104 --- /dev/null +++ b/html/js/app/sfzbuilder/model.js @@ -0,0 +1,279 @@ +// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later +// @ts-check + +/** + * Pure functions for the SFZ builder. + * This file has no DOM code and no network code. + * @module + */ + +/** + * @typedef {object} Slot + * @property {string} sample Bank-local file name. + * @property {string|null} source Absolute path, if the file is outside the bank. + * @property {number|null} volume Gain in dB. + * @property {number|null} pitch Root note, or null for automatic. + * @property {string} loop One of "one_shot" or "no_loop". + */ + +/** + * @typedef {object} SampleFile + * @property {string} basename + * @property {string} fullname + * @property {number} [size] The size in bytes, if the server gives it. + */ + +export const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] + +/** + * The audio types that the server accepts. + * Keep this list the same as AUDIO_EXTENSIONS in mod/sfzbuilder.py. + * The server refuses a batch that holds one other file, so the panel removes + * those files before the upload. + */ +export const AUDIO_EXTENSIONS = [ + '.aif', '.aifc', '.aiff', '.au', '.bwf', '.flac', '.htk', '.iff', '.mat4', '.mat5', + '.oga', '.ogg', '.opus', '.paf', '.pvf', '.pvf5', '.sd2', '.sf', '.snd', '.svx', + '.vcc', '.w64', '.wav', '.xi', + '.3g2', '.3gp', '.aac', '.ac3', '.amr', '.ape', '.mp2', '.mp3', '.mpc', '.wma', +] + +export const DEFAULT_BASE_NOTE = 36 +export const MAX_NOTE = 127 +export const DEFAULT_PADS = 8 +export const MIN_PADS = 1 +export const MAX_PADS = 128 +export const MIN_COLS = 2 +export const MAX_COLS = 8 +export const DEFAULT_COLS = 4 + +/** + * Gives the name of a MIDI note. Example: 36 gives "C2". + * @param {number} note + * @returns {string} + */ +export function noteName(note) { + return NOTE_NAMES[note % 12] + (Math.floor(note / 12) - 1) +} + +/** + * Reads a base note from a text value. + * An invalid value gives the default base note. + * @param {string|number|null|undefined} value + * @returns {number} A number from 0 to 127. + */ +export function clampBaseNote(value) { + const base = parseInt(String(value), 10) + return isNaN(base) ? DEFAULT_BASE_NOTE : Math.max(0, Math.min(MAX_NOTE, base)) +} + +/** + * Reads a pad count from a text value. + * @param {string|number|null|undefined} value + * @returns {number} A number from 1 to 128. + */ +export function clampPadCount(value) { + return Math.max(MIN_PADS, Math.min(MAX_PADS, parseInt(String(value), 10) || MIN_PADS)) +} + +/** + * Changes the number of pads. Each pad keeps its index. + * @param {(Slot|null)[]} slots + * @param {number} count + * @returns {(Slot|null)[]} A new array. The input array does not change. + */ +export function resizePads(slots, count) { + /** @type {(Slot|null)[]} */ + const next = [] + for (let i = 0; i < count; i++) { + next.push(slots[i] || null) + } + return next +} + +/** + * Gives the note number of a pad. + * @param {number} baseNote + * @param {number} index + * @returns {number} + */ +export function padNote(baseNote, index) { + return baseNote + index +} + +/** + * Reads a column count from a text value. + * @param {string|number|null|undefined} value + * @returns {number} A number from 2 to 6. + */ +export function clampCols(value) { + const cols = parseInt(String(value), 10) + return isNaN(cols) ? DEFAULT_COLS : Math.max(MIN_COLS, Math.min(MAX_COLS, cols)) +} + +/** + * Gives the parts of the note of a pad. + * + * A bank can hold 128 pads and the base note can be more than 0. Thus the last + * pads can go past note 127. Such a pad gets no note and the panel shows this. + * @param {number} note + * @returns {{ note: number, name: string, inRange: boolean }} + */ +export function noteToken(note) { + const inRange = note >= 0 && note <= MAX_NOTE + return { note: note, name: inRange ? noteName(note) : '--', inRange: inRange } +} + +/** + * Gives a short size for a file. Example: 412000 gives "402 KB". + * @param {number|null|undefined} bytes + * @returns {string} An empty string if the size is unknown. + */ +export function formatSize(bytes) { + if (typeof bytes !== 'number' || !isFinite(bytes) || bytes < 0) { + return '' + } + if (bytes < 1024) { + return bytes + ' B' + } + if (bytes < 1024 * 1024) { + return Math.round(bytes / 1024) + ' KB' + } + return (bytes / (1024 * 1024)).toFixed(1) + ' MB' +} + +/** + * Changes the saved pads from the server into the pads that the panel uses. + * @param {any[]} saved + * @returns {(Slot|null)[]} + */ +export function alignSavedSlots(saved) { + /** @type {(Slot|null)[]} */ + const aligned = [] + for (const slot of saved) { + if (!slot) { + aligned.push(null) + continue + } + aligned.push({ + sample: slot.sample, + source: null, + volume: slot.volume, + pitch: slot.pitch_keycenter === undefined ? null : slot.pitch_keycenter, + loop: slot.loop_mode || 'one_shot', + }) + } + return aligned +} + +/** + * Makes the data for the build request. + * An empty pad stays in the array as null. Thus the other pads keep their notes. + * @param {(Slot|null)[]} slots + * @returns {{ aligned: (object|null)[], filled: number }} + */ +export function buildPayload(slots) { + /** @type {(object|null)[]} */ + const aligned = [] + let filled = 0 + for (const slot of slots) { + if (!slot) { + aligned.push(null) + continue + } + filled++ + aligned.push({ + sample: slot.sample, + source: slot.source, + volume: slot.volume, + pitch_keycenter: slot.pitch, + loop_mode: slot.loop, + }) + } + return { aligned, filled } +} + +/** + * Selects the samples that match a search term. + * The search ignores the case of the letters. + * @param {SampleFile[]} files + * @param {string} term + * @returns {SampleFile[]} + */ +export function filterFiles(files, term) { + const needle = (term || '').toLowerCase() + if (!needle) { + return files.slice() + } + return files.filter((f) => f.basename.toLowerCase().indexOf(needle) >= 0) +} + +/** + * Makes the pad data for a sample that the user assigns. + * @param {SampleFile} entry + * @param {string} source The current sample source: "bank", "device" or "usb". + * @returns {Slot} + */ +export function slotFromSample(entry, source) { + return { + sample: entry.basename, + source: source === 'bank' ? null : entry.fullname, + volume: 0, + pitch: null, + loop: 'one_shot', + } +} + +/** + * Chooses the bank that the panel opens with. + * + * The panel remembers the bank that you worked on and opens it again, because + * you frequently come back to the same bank. A bank that is gone, or a device + * that has no memory of a bank, gives the first bank instead. An empty device + * gives no bank and the panel then shows its empty state. + * + * @param {string[]} banks The banks that the server gives. + * @param {string|null|undefined} remembered The bank of the last visit. + * @returns {string} A name from `banks`, or an empty string. + */ +export function pickInitialBank(banks, remembered) { + if (remembered && banks.indexOf(remembered) >= 0) { + return remembered + } + return banks.length > 0 ? banks[0] : '' +} + +/** + * Tells if the server accepts a file name. + * @param {string} name + * @returns {boolean} + */ +export function isAudioName(name) { + const lower = (name || '').toLowerCase() + return AUDIO_EXTENSIONS.some((ext) => lower.endsWith(ext)) +} + +/** + * Divides dropped files into the files that the server accepts and the others. + * + * The upload control uses `accept="audio/*"`, but a drop from the desktop has + * no filter. A folder frequently holds an image or a hidden file. The server + * refuses the full batch in that condition, so the panel removes those files. + * @param {{ name: string }[]} files + * @returns {{ accepted: any[], rejected: any[] }} + */ +export function splitAudioFiles(files) { + /** @type {any[]} */ + const accepted = [] + /** @type {any[]} */ + const rejected = [] + for (const file of files) { + if (isAudioName(file.name)) { + accepted.push(file) + } else { + rejected.push(file) + } + } + return { accepted, rejected } +} diff --git a/html/js/app/sfzbuilder/mount.js b/html/js/app/sfzbuilder/mount.js new file mode 100644 index 00000000..1c1c7265 --- /dev/null +++ b/html/js/app/sfzbuilder/mount.js @@ -0,0 +1,811 @@ +// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later +// @ts-check + +/** + * The SFZ builder island. + * The legacy code starts the island with `mount`. This is the only seam. + * @module + */ + +import * as api from './api.js' +import * as model from './model.js' +import * as view from './view.js' +import { createStore } from './state.js' +import { toggleClass } from './dom.js' + +/** Where the browser keeps the column count between visits. */ +const COLS_KEY = 'sfzbuilder.cols' + +/** Where the browser keeps the bank that you worked on last. */ +const BANK_KEY = 'sfzbuilder.bank' + +/** + * Adds the stylesheet of the island to the page, one time. + * + * The island holds its markup, its code and its style in one directory. Thus + * you can delete the directory and remove all of the feature. + */ +function addStylesheet() { + if (document.querySelector('link[data-sfzbuilder]')) { + return + } + const link = document.createElement('link') + link.rel = 'stylesheet' + link.type = 'text/css' + link.href = new URL('./sfzbuilder.css', import.meta.url).href + link.setAttribute('data-sfzbuilder', '1') + // The code can load while the stylesheet does not. The panel is then + // there but has no size and no place, so the screen does not change and + // the fault gives no other sign. Say it instead. + link.addEventListener('error', () => { + console.error('The Sound Bank Builder cannot read', link.href) + const root = document.getElementById('sfzbuilder-library') + if (root) { + root.setAttribute('style', 'position:absolute;top:0;bottom:45px;left:0;right:0;' + + 'z-index:2;background:#0a0a0a;color:#D91E36;padding:40px;font:13px monospace') + root.textContent = 'The Sound Bank Builder cannot read its stylesheet: ' + link.href + } + }) + document.head.appendChild(link) +} + +/** + * Reads one value that the last visit wrote. + * + * A browser can refuse storage: a private window, or a setting that blocks site + * data. Each call is thus in a try. The panel works without storage; it only + * forgets what the last visit did. + * + * @param {string} key + * @returns {string|null} Null when there is no value, or when storage fails. + */ +function readSetting(key) { + try { + return window.localStorage.getItem(key) + } catch (e) { + return null + } +} + +/** + * Keeps one value for the next visit. + * @param {string} key + * @param {string} value + */ +function writeSetting(key, value) { + try { + window.localStorage.setItem(key, value) + } catch (e) { + /* The panel works without storage. */ + } +} + +/** + * Puts the SFZ builder in an element. + * @param {HTMLElement} root The panel element. + * @returns {{ open: () => void, close: () => void, destroy: () => void }} + */ +export function mount(root) { + addStylesheet() + + const store = createStore(() => { /* the draws below are explicit */ }) + + // Which view of the panel the answers coming back belong to. + // + // The panel asks for the sample list, the files of the bank and the pad + // layout in three requests that do not wait for each other. Click bank A + // and then bank B and the answers for A can land after the answers for B, + // which drew the pads of A over the bank of B. Each request keeps the + // number the panel had when it went out, and an answer that comes back to + // a different number is dropped: the request that replaced it is already + // on its way. + let generation = 0 + + /** @returns {() => boolean} A test that says whether the answer still fits. */ + function fresh() { + const mine = generation + return () => mine === generation + } + + // One player for the full panel, so only one sample sounds at a time. + /** @type {HTMLAudioElement|null} */ + let player = null + /** @type {HTMLElement|null} */ + let playingButton = null + + const el = view.createLayout(root, { + onAddBank: () => view.promptNewBank(el, createBank), + onSource: setSource, + onUpload: uploadFromInput, + onSearch: (term) => { + store.update({ search: term }) + drawSamples() + }, + onPadCount: setPadCount, + onCols: setCols, + onBaseNote: setBaseNote, + onSave: save, + onRenameBank: () => view.promptRenameBank(el, renameBank), + onDeleteBank: removeBank, + }) + + /** + * @param {string} message + * @param {boolean} [isError] + */ + function setStatus(message, isError) { + store.update({ status: message, statusIsError: !!isError }) + view.renderStatus(el, message, !!isError) + } + + /** + * Says that the pads hold a change that no save wrote. + * + * Everything the sidecar keeps counts: which sample is on which pad, the + * gain, the root note, the loop mode, the pad count and the base note. The + * column count does not -- that is how you look at the bank, not what the + * bank is, and it lives in the browser rather than on the device. + */ + function markDirty() { + // No bank open means no work to lose. The panel builds its first pads + // before it has asked the server for anything -- setPadCount below + // grows the empty array to eight -- and that is the panel arranging + // itself, not a change to a bank. Without this test the first bank the + // panel opened asked whether to drop work that never existed. + if (!store.get().currentBank) { + return + } + if (store.get().dirty) { + return + } + store.update({ dirty: true }) + drawBankView() + } + + /** @param {boolean} [saved] */ + function markClean(saved) { + if (!store.get().dirty) { + return + } + store.update({ dirty: false }) + if (saved !== false) { + drawBankView() + } + } + + /** + * Asks before a step that would drop the changes the pads hold. + * + * The words are the ones the legacy panels use for the same question, so + * the answer means the same thing wherever you meet it. + * @returns {boolean} Whether to go on. + */ + function confirmDiscard() { + if (!store.get().dirty) { + return true + } + return window.confirm('There are unsaved modifications that will be lost. Are you sure?') + } + + function stopPreview() { + if (player) { + player.pause() + player = null + } + if (playingButton) { + playingButton.classList.remove('sfz-playing') + playingButton.innerHTML = '▶' + playingButton = null + } + } + + /** + * @param {HTMLElement} button + * @param {string} fullname + */ + function playPreview(button, fullname) { + const wasPlaying = button.classList.contains('sfz-playing') + stopPreview() + if (wasPlaying) { + return + } + player = new Audio(api.audioUrl(fullname)) + playingButton = button + button.classList.add('sfz-playing') + button.innerHTML = '■' + player.addEventListener('ended', stopPreview) + player.addEventListener('error', () => { + stopPreview() + setStatus('Cannot play that sample', true) + }) + player.play() + } + + /** + * Gives the URL that plays the sample of a pad. + * + * A pad that comes from another place keeps the full path. A pad that comes + * from the bank keeps the name only, so the name goes to the file list of + * the bank. A bank that you did not open yet gives no URL and the play + * control of the pad is off. + * @param {import('./model.js').Slot} slot + * @returns {string} + */ + function previewUrl(slot) { + if (slot.source) { + return slot.source + } + for (const f of store.get().bankFiles) { + if (f.basename === slot.sample) { + return f.fullname + } + } + return '' + } + + function drawSlots() { + view.renderSlots(el, store.get(), { + onAssign: assignSlot, + onSelect: selectSlot, + onClear: clearSlot, + onPlay: playPreview, + previewUrl: previewUrl, + onEdit: editSlot, + }) + } + + function drawSamples() { + view.renderSamples(el, store.get(), { + onPlay: playPreview, + onPick: pickSample, + }) + // The old panel stopped the sound on every draw, so a filter cut the + // sample that played. The panel now stops only when the row goes away. + if (playingButton && !document.contains(playingButton)) { + stopPreview() + } + } + + function drawBanks() { + view.renderBanks(el, store.get(), selectBank) + } + + function drawBankView() { + view.renderBankView(el, store.get()) + } + + /** @param {number} idx */ + function selectSlot(idx) { + const current = store.get().selectedSlot + store.update({ selectedSlot: current === idx ? -1 : idx }) + drawSlots() + } + + /** @param {number} idx */ + function clearSlot(idx) { + const slots = store.get().slots.slice() + slots[idx] = null + store.update({ slots: slots }) + markDirty() + drawSlots() + } + + /** + * Changes one field of one pad. + * + * The pads are not drawn again: the change came from a control inside the + * pad and a redraw would take the focus out of it while you type. + * @param {number} idx + * @param {Partial} patch + */ + function editSlot(idx, patch) { + const state = store.get() + const slot = state.slots[idx] + if (!slot) { + return + } + const slots = state.slots.slice() + slots[idx] = Object.assign({}, slot, patch) + store.update({ slots: slots }) + markDirty() + } + + /** + * @param {number} idx + * @param {import('./model.js').SampleFile} entry + */ + function assignSlot(idx, entry) { + const state = store.get() + if (idx < 0 || idx >= state.slots.length) { + return + } + const slots = state.slots.slice() + slots[idx] = model.slotFromSample(entry, state.source) + store.update({ slots: slots, selectedSlot: -1 }) + markDirty() + setStatus(entry.basename + ' put on pad ' + (idx + 1)) + drawSlots() + } + + /** @param {import('./model.js').SampleFile} entry */ + function pickSample(entry) { + const idx = store.get().selectedSlot + if (idx < 0) { + setStatus('Click a pad first, or drag the sample onto a pad', true) + return + } + assignSlot(idx, entry) + } + + /** @param {number} count */ + function setPadCount(count) { + const slots = store.get().slots + store.update({ padCount: count }) + if (slots.length !== count) { + store.update({ slots: model.resizePads(slots, count) }) + markDirty() + } + drawSlots() + } + + /** @param {number} cols */ + function setCols(cols) { + store.update({ cols: cols }) + writeSetting(COLS_KEY, String(cols)) + el.grid.style.setProperty('--sfz-cols', String(cols)) + } + + /** @param {number|null} note */ + function setBaseNote(note) { + const next = model.clampBaseNote(note) + if (next !== store.get().baseNote) { + store.update({ baseNote: next }) + markDirty() + } + drawSlots() + } + + /** @param {string} source */ + function setSource(source) { + generation++ + store.update({ source: source }) + drawBankView() + refreshSamples() + } + + function refreshSamples() { + const state = store.get() + const still = fresh() + api.listSamples(state.source, state.currentBank).then((resp) => { + if (!still()) { + return + } + if (!resp.ok) { + setStatus(resp.error, true) + return + } + const files = resp.files || [] + const patch = { files: files } + if (store.get().source === 'bank') { + // @ts-ignore - the patch takes both keys. + patch.bankFiles = files + } + store.update(patch) + drawSamples() + drawSlots() + }, () => setStatus('Failed to load the samples', true)) + } + + /** + * Reads the files of the current bank, whatever list the panel shows. + * The pads need this list to play a sample that the bank holds. + * @returns {Promise} + */ + function refreshBankFiles() { + const bank = store.get().currentBank + if (!bank) { + store.update({ bankFiles: [] }) + return Promise.resolve() + } + const still = fresh() + return api.listSamples('bank', bank).then((resp) => { + if (resp.ok && still()) { + store.update({ bankFiles: resp.files || [] }) + drawSlots() + } + }, () => { /* The panel works without the play control of a pad. */ }) + } + + /** @returns {Promise} */ + function loadBanks() { + return api.listBanks().then((resp) => { + const banks = resp.banks + const current = store.get().currentBank + store.update({ + banks: banks, + bankCounts: resp.counts || {}, + currentBank: current && banks.indexOf(current) < 0 ? '' : current, + }) + drawBanks() + drawBankView() + }, () => { + setStatus('Failed to load the banks', true) + }) + } + + // Reads the pad layout that the last save wrote, so a bank opens for edit. + function loadBankState() { + const bank = store.get().currentBank + if (!bank) { + return + } + const still = fresh() + api.loadBank(bank).then((resp) => { + if (!still()) { + return + } + if (!resp.ok) { + setStatus(resp.error, true) + return + } + const saved = resp.slots || [] + if (saved.length === 0) { + // A bank that was never saved has no layout. The panel would + // else keep the pads of the bank that was open before, and the + // pad count and base note with them. + const padCount = model.DEFAULT_PADS + store.update({ + slots: model.resizePads([], padCount), + padCount: padCount, + baseNote: model.DEFAULT_BASE_NOTE, + }) + el.padStepper.set(padCount) + el.baseStepper.set(model.DEFAULT_BASE_NOTE) + // The two lines above run the same handlers a click on the + // stepper runs, so they say the bank changed. The bank was + // read, not changed, so the flag goes down after them, never + // before. + markClean() + drawSlots() + return + } + const aligned = model.alignSavedSlots(saved) + const baseNote = model.clampBaseNote(resp.base_note) + store.update({ slots: aligned, padCount: aligned.length, baseNote: baseNote }) + el.padStepper.set(aligned.length) + el.baseStepper.set(baseNote) + markClean() + drawSlots() + }, () => setStatus('Failed to load the bank', true)) + } + + /** @param {string} name */ + function selectBank(name) { + if (store.get().currentBank === name) { + return + } + if (!confirmDiscard()) { + // The row that was clicked wears the selection for a moment before + // the answer comes back, so the list is drawn again from the state, + // which still holds the bank that stays open. + drawBanks() + return + } + stopPreview() + generation++ + store.update({ currentBank: name, selectedSlot: -1, bankFiles: [], dirty: false }) + // The next visit opens this bank again. See model.pickInitialBank. + writeSetting(BANK_KEY, name) + drawBanks() + drawBankView() + setStatus('') + loadBankState() + refreshSamples() + if (store.get().source !== 'bank') { + refreshBankFiles() + } + } + + /** @param {string} name */ + function createBank(name) { + if (!name) { + setStatus('Type a bank name first', true) + return + } + api.createBank(name).then((resp) => { + if (!resp.ok) { + setStatus(resp.error, true) + return + } + store.update({ currentBank: '' }) + loadBanks().then(() => selectBank(resp.name)) + }, () => setStatus('Failed to create the bank', true)) + } + + /** + * Changes the name of the current bank. + * + * The name that reaches the disk is not always the name that was asked + * for -- a space becomes an underscore -- so the panel opens the bank + * under the name that the server gives back, not the one it sent. + * @param {string} name + */ + function renameBank(name) { + const from = store.get().currentBank + if (!from) { + return + } + // The panel opens the bank again under its new name, which reads the + // layout back from the disk and thus drops whatever the pads hold. + if (!confirmDiscard()) { + return + } + api.renameBank(from, name).then((resp) => { + if (!resp.ok) { + setStatus(resp.error, true) + return + } + // currentBank is cleared first so that selectBank does its work: + // it returns early when the name it is given is already open, and + // after a rename of "Kit" to "Kit A" the store still says "Kit". + store.update({ currentBank: '', dirty: false }) + loadBanks().then(() => { + selectBank(resp.name) + setStatus('Renamed to ' + resp.name) + }) + }, () => setStatus('Rename failed', true)) + } + + /** + * Removes the current bank and everything in it. + * + * A confirm box asks first, the same way the legacy panels ask before a + * change that cannot be undone. The bank holds the samples that were + * uploaded to it, so this is not a change you can take back. + */ + function removeBank() { + const name = store.get().currentBank + if (!name) { + return + } + const count = store.get().bankCounts[name] + const held = typeof count === 'number' && count > 0 + ? ' and the ' + count + ' sample(s) in it' + : '' + if (!window.confirm('Delete the bank ' + name + held + '? This cannot be undone.')) { + return + } + api.deleteBank(name).then((resp) => { + if (!resp.ok) { + setStatus(resp.error, true) + return + } + stopPreview() + // The next visit must not try to open the bank that just went + // away. refreshAll then falls back to the first bank there is. + writeSetting(BANK_KEY, '') + generation++ + store.update({ currentBank: '', selectedSlot: -1, bankFiles: [], files: [], dirty: false }) + loadBanks().then(() => { + const banks = store.get().banks + if (banks.length > 0) { + selectBank(banks[0]) + } else { + drawSamples() + drawSlots() + } + setStatus('Deleted ' + resp.name) + }) + }, () => setStatus('Delete failed', true)) + } + + function uploadFromInput() { + const files = el.uploadInput.files + if (!files || files.length === 0) { + return + } + const list = Array.prototype.slice.call(files) + el.uploadInput.value = '' + upload(list) + } + + /** + * Sends files to the current bank. + * @param {File[]} files + */ + function upload(files) { + const bank = store.get().currentBank + if (!bank) { + setStatus('Select a bank first', true) + return + } + // The server refuses the full batch if one file is not audio, and it + // refuses it after it writes the files before that one. Thus the panel + // removes those files first. A drop from the desktop has no filter. + const split = model.splitAudioFiles(files) + if (split.accepted.length === 0) { + setStatus('No audio file in that drop', true) + return + } + setStatus('Uploading ' + split.accepted.length + ' file(s)…') + api.uploadSamples(bank, split.accepted).then((resp) => { + if (!resp.ok) { + setStatus(resp.error, true) + return + } + const skipped = split.rejected.length + setStatus(resp.files.length + ' file(s) uploaded' + + (skipped ? ', ' + skipped + ' not audio' : ''), skipped > 0) + generation++ + store.update({ source: 'bank' }) + drawBankView() + refreshSamples() + loadBanks() + }, () => setStatus('Upload failed', true)) + } + + function save() { + const state = store.get() + if (!state.currentBank) { + setStatus('Select a bank first', true) + return + } + const payload = model.buildPayload(state.slots) + if (payload.filled === 0) { + setStatus('Put a sample on a pad first', true) + return + } + api.buildBank(state.currentBank, state.baseNote, payload.aligned).then((resp) => { + if (!resp.ok) { + setStatus(resp.error, true) + return + } + markClean() + setStatus('Saved ' + resp.file + ' with ' + resp.count + ' sample(s)') + refreshBankFiles() + }, () => setStatus('Save failed', true)) + } + + // ---- the drop of a file from the desktop ---------------------------- + // + // This is not the same as the drag of a sample onto a pad. That drag uses + // jQuery UI, which reads mouse events. A drop from the desktop uses the + // drag events of the browser and carries a `dataTransfer`. The code below + // acts on a drop that holds files only, so an internal drag goes through. + + let dragDepth = 0 + + /** + * @param {DragEvent} e + * @returns {boolean} + */ + function holdsFiles(e) { + const types = e.dataTransfer ? e.dataTransfer.types : null + if (!types) { + return false + } + return Array.prototype.indexOf.call(types, 'Files') >= 0 + } + + /** @param {boolean} on */ + function showOverlay(on) { + toggleClass(el.overlay, 'sfz-overlay--on', on) + } + + el.panel.addEventListener('dragenter', (/** @type {any} */ e) => { + if (!holdsFiles(e)) { + return + } + e.preventDefault() + // A drag over a child element sends a leave and then an enter. The + // count keeps the cover until the drag goes past the panel itself. + dragDepth++ + showOverlay(true) + }) + + el.panel.addEventListener('dragover', (/** @type {any} */ e) => { + if (!holdsFiles(e)) { + return + } + e.preventDefault() + if (e.dataTransfer) { + e.dataTransfer.dropEffect = 'copy' + } + }) + + el.panel.addEventListener('dragleave', (/** @type {any} */ e) => { + if (!holdsFiles(e)) { + return + } + dragDepth = Math.max(0, dragDepth - 1) + if (dragDepth === 0) { + showOverlay(false) + } + }) + + el.panel.addEventListener('drop', (/** @type {any} */ e) => { + if (!holdsFiles(e)) { + return + } + e.preventDefault() + dragDepth = 0 + showOverlay(false) + const files = e.dataTransfer ? Array.prototype.slice.call(e.dataTransfer.files) : [] + if (files.length > 0) { + upload(files) + } + }) + + // A drop outside the panel must not make the browser open the file. + /** @param {DragEvent} e */ + function blockOutside(e) { + if (holdsFiles(e) && !el.panel.contains(/** @type {Node} */ (e.target))) { + e.preventDefault() + } + } + /** @param {DragEvent} e */ + function blockOutsideDrop(e) { + if (holdsFiles(e) && !el.panel.contains(/** @type {Node} */ (e.target))) { + e.preventDefault() + dragDepth = 0 + showOverlay(false) + } + } + let guarded = false + + function addGuards() { + if (!guarded) { + guarded = true + document.addEventListener('dragover', blockOutside) + document.addEventListener('drop', blockOutsideDrop) + } + } + + function removeGuards() { + if (guarded) { + guarded = false + document.removeEventListener('dragover', blockOutside) + document.removeEventListener('drop', blockOutsideDrop) + } + } + + // ---- the seam -------------------------------------------------------- + + function refreshAll() { + addGuards() + loadBanks().then(() => { + const state = store.get() + if (!state.currentBank) { + // The panel opens on the bank of the last visit. loadBanks + // clears the current bank when that bank is gone, so this runs + // on the first visit and after a bank goes away. + const pick = model.pickInitialBank(state.banks, readSetting(BANK_KEY)) + if (pick) { + selectBank(pick) + return + } + } + refreshSamples() + drawSlots() + }) + } + + function onClose() { + stopPreview() + dragDepth = 0 + showOverlay(false) + removeGuards() + } + + setCols(model.clampCols(readSetting(COLS_KEY))) + el.colStepper.set(store.get().cols) + setPadCount(store.get().padCount) + drawBankView() + + return { + open: refreshAll, + close: onClose, + destroy: onClose, + } +} diff --git a/html/js/app/sfzbuilder/sfzbuilder.css b/html/js/app/sfzbuilder/sfzbuilder.css new file mode 100644 index 00000000..567820d0 --- /dev/null +++ b/html/js/app/sfzbuilder/sfzbuilder.css @@ -0,0 +1,844 @@ +/* SPDX-FileCopyrightText: 2012-2023 MOD Audio UG + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * The stylesheet of the SFZ builder island. + * + * mount.js adds this file to the page. No other panel reads it and it reads no + * other panel. Thus you can delete the island directory and remove all of the + * feature. + * + * Every class starts with "sfz-". A modifier ends with "--". Thus a modifier + * can never collide with the class of an element, which was the cause of an + * earlier layout fault. + * + * Each rule starts at #sfzbuilder-library. main.css holds rules for bare + * elements (button, input, select, h2, ul). The identifier gives these rules a + * higher weight, so the panel keeps its appearance. + */ + +/* Design tokens. Change a value here and the full panel follows. + * + * These live on :root, not on #sfzbuilder-library. jQuery UI puts the helper of + * a drag on the body, outside the panel, so a token on the panel does not reach + * it. A var() that does not resolve makes the property invalid, and the helper + * was then black text on nothing. Each name starts with --sfz-, so a token at + * the root cannot collide with the legacy styles. */ +:root { + --sfz-accent: #883996; + /* The accent as a foreground. --sfz-accent is the brand purple and it is + right as a fill or a border, but as text on the near-black of the panel it + sits at about 2.4:1, under any reading of the contrast rule. This is the + same hue lifted until it reads: a note name, a play mark, the Add button. + Use --sfz-accent for a fill, --sfz-accent-fg for a glyph. */ + --sfz-accent-fg: #c27bd0; + --sfz-bg: #0a0a0a; + --sfz-panel: #0f0f0f; + --sfz-rail: #141414; + --sfz-cell: #151515; + --sfz-line: #343434; + --sfz-line-soft: #262626; + --sfz-line-free: #2e2e2e; + --sfz-text: #f2f2f2; + --sfz-dim: #a6a6a6; + --sfz-dimmer: #808080; + --sfz-err: #ff5468; + /* Every font-family that names one of these two carries !important. + main.less holds `* { font-family: "cooper hewitt", Sans-serif !important }`, + which beats a plain declaration whatever its weight or its order, so the + panel drew in Cooper Hewitt and read nothing from these tokens at all. The + header was where you saw it first: a title that was meant to be monospace + came out in the page font and grew wide enough to push the tool row onto a + second line. This is the same trap as the outline rule, see .sfz-focus. */ + --sfz-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; + --sfz-sans: system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif; + --sfz-snap: 80ms; + /* The least width of the note stepper value. See .sfz-tokenbox. */ + --sfz-token-w: 76px; + /* The least width of a pad. See .sfz-grid: a column stretches to fill the + space it is given but never goes under this, and the grid scrolls sideways + once the chosen number of columns no longer fits. Below about this width + the file name of a pad shows two or three letters and the mini steppers + lose their digits. Tune this one value to trade legibility for columns. */ + --sfz-pad-min: 140px; + /* The focus ring. A light neutral, not the accent: the ring has to be legible + on the accent itself, which fills a bank row and a pad while they hold the + selection. */ + --sfz-focus: #e8e8e8; +} + +#sfzbuilder-library { + position: absolute; + top: 0; + bottom: 45px; + left: 0; + right: 0; + overflow: hidden; + z-index: 2; + background: var(--sfz-bg); + color: var(--sfz-text); + font-size: 12px; + font-variant-numeric: tabular-nums; + -webkit-font-smoothing: antialiased; +} + +/* The descendant half of the selector is what does the work. main.less names + every element one by one, so its rule lands on each child of the panel + directly and inheritance from the panel never reaches them. */ +#sfzbuilder-library, +#sfzbuilder-library * { + font-family: var(--sfz-mono) !important; +} + +#sfzbuilder-library * { box-sizing: border-box; } + +/* The shared header bar is the one part of the panel that is not the island's + own. main.less draws it for every library panel (#bank-library, + #file-manager-library, #patchstorage-library ...), and the bar of this panel + has to be indistinguishable from theirs -- the same face, the same weight, + the same grey, the same place on the line -- because you see two of them in a + row as you move between panels and anything off reads as a fault. + + So the whole header opts out of the island and takes the page style back: + + font-family The mono rule above lands on the header too, and a monospace + "SOUND BANK BUILDER" is both wider and plainly a different + face from "USER BANKS" one panel over. The value here is the + one from main.less `* { font-family: "cooper hewitt", + Sans-serif !important }`, character for character, so the + fallback matches as well if the web font fails to load. + font-smoothing The panel sets antialiased on itself and that inherits into + the header, where it thins the strokes: the title came out + visibly lighter than the same words on the panel next door. + auto puts the header back on the rendering of the page. + font-variant-numeric + tabular-nums is set on the panel and inherits. It changes the + digits, which the title of this panel has none of today, but + the other headers do not carry it. letter-spacing is normal + for the same reason: an earlier pass tracked the title out by + .04em, and next to an untracked "USER BANKS" it read wrong. + color, text-transform + main.less already gives these to `#sfzbuilder-library header + h1`. They are repeated so the header stands on its own if that + selector list ever loses this panel; #999 is its value. + + Anything you want to change about the look of this bar belongs in main.less + with the other panels, not here. This block only stops the island from + reaching in. */ +#sfzbuilder-library header, +#sfzbuilder-library header * { + font-family: "cooper hewitt", Sans-serif !important; + -webkit-font-smoothing: auto; + letter-spacing: normal; + font-variant-numeric: normal; +} +#sfzbuilder-library header h1 { + text-transform: uppercase; + font-weight: normal; + color: #999; +} + +/* The scroll bars. + * + * The panel is dark, but the browser draws a scroll bar and a form control from + * the setting of the system. On a system that is set to light, a white scroll + * bar cut through the bank list. color-scheme tells the browser which set to + * draw, so the panel gets the dark set on every system. + * + * scrollbar-color and scrollbar-width are the standard properties. The + * ::-webkit- rules give the same result on an older browser that does not + * have them, which the device can have. */ +#sfzbuilder-library { + color-scheme: dark; + scrollbar-width: thin; + scrollbar-color: var(--sfz-line) transparent; +} +#sfzbuilder-library *::-webkit-scrollbar { width: 10px; height: 10px; } +#sfzbuilder-library *::-webkit-scrollbar-track { background: transparent; } +#sfzbuilder-library *::-webkit-scrollbar-thumb { + background: var(--sfz-line); + border: 2px solid transparent; + background-clip: content-box; +} +#sfzbuilder-library *::-webkit-scrollbar-thumb:hover { background: var(--sfz-dimmer); background-clip: content-box; } +#sfzbuilder-library *::-webkit-scrollbar-corner { background: transparent; } + +#sfzbuilder-library button, +#sfzbuilder-library input, +#sfzbuilder-library select { + font-family: var(--sfz-mono) !important; + font-variant-numeric: tabular-nums; + border-radius: 0; + box-shadow: none; + text-shadow: none; +} + +/* The focus ring. + * + * main.less holds `* { outline: 0 !important }`, which takes the ring that the + * browser draws off every element of the page, and thus off this panel. A + * person on the keyboard then has no sign at all of where the focus sits. The + * rule below puts a ring back, inside this panel only. It carries !important + * because nothing except !important beats an !important declaration. + * + * The offset is negative, so the ring is drawn inside the edge of the element. + * A ring outside the edge is cut by the list that holds a bank row or a sample + * row, and by the grid that holds a pad. + * + * :focus-visible draws the ring for the keyboard and not for a press, which + * matters wherever the page is opened by touch, a tablet or a phone: a tap on a + * pad would else leave a ring behind on it. The second rule is what takes the ring off a press. A + * browser that does not know :focus-visible drops that rule and then shows the + * ring for a press as well, which is the safe way for it to fail. */ +#sfzbuilder-library :focus { + outline: 2px solid var(--sfz-focus) !important; + outline-offset: -2px; +} +#sfzbuilder-library :focus:not(:focus-visible) { + outline: 0 !important; +} + +/* The panel is a wrapper, not the root. window.js hides and shows the root + with jQuery, which writes an inline display value. A grid on the root would + fight that. */ +/* The shared header bar is 45px high and has position: fixed, so it draws over + the top of the panel. The panel starts below it. */ +#sfzbuilder-library .sfz-panel { + position: relative; + display: grid; + grid-template-columns: 240px minmax(0, 1fr) 320px; + margin-top: 45px; + height: calc(100% - 45px); + background: var(--sfz-panel); +} + +/* ---------- shared parts ---------- */ + +#sfzbuilder-library .sfz-lbl { + font-size: 10px; + letter-spacing: .18em; + text-transform: uppercase; + color: var(--sfz-dim); +} + +#sfzbuilder-library .sfz-seg { display: inline-flex; border: 1px solid var(--sfz-line); } +#sfzbuilder-library .sfz-seg button { + appearance: none; + background: transparent; + border: 0; + border-right: 1px solid var(--sfz-line); + color: var(--sfz-dim); + padding: 7px 13px; + cursor: pointer; + font-size: 10px; + letter-spacing: .16em; + text-transform: uppercase; + transition: background var(--sfz-snap) linear, color var(--sfz-snap) linear; +} +#sfzbuilder-library .sfz-seg button:last-child { border-right: 0; } +#sfzbuilder-library .sfz-seg button:hover { color: var(--sfz-text); background: var(--sfz-line-soft); } +#sfzbuilder-library .sfz-seg button[aria-pressed="true"] { background: var(--sfz-accent); color: #fff; } + +#sfzbuilder-library .sfz-btn { + appearance: none; + display: inline-block; + border: 1px solid var(--sfz-accent); + background: transparent; + color: var(--sfz-accent-fg); + padding: 8px 18px; + margin: 0; + cursor: pointer; + font-size: 11px; + letter-spacing: .18em; + text-transform: uppercase; + font-weight: 700; + text-align: center; + transition: background var(--sfz-snap) linear, color var(--sfz-snap) linear; +} +#sfzbuilder-library .sfz-btn:hover { background: var(--sfz-accent); color: #fff; } +#sfzbuilder-library .sfz-btn:disabled, +#sfzbuilder-library .sfz-btn.sfz-btn--off { + border-color: var(--sfz-line); + color: var(--sfz-dimmer); + cursor: default; + background: transparent; +} +/* A second rank of button. Rename and Delete sit beside Save and must not read + as the thing to press: Save is the one the panel is for. */ +#sfzbuilder-library .sfz-btn--quiet { + border-color: var(--sfz-line); + color: var(--sfz-dim); + padding: 8px 13px; +} +#sfzbuilder-library .sfz-btn--quiet:hover { + background: transparent; + border-color: var(--sfz-dim); + color: var(--sfz-text); +} +#sfzbuilder-library .sfz-btn--danger:hover { + border-color: var(--sfz-err); + color: var(--sfz-err); +} + +/* The stepper: minus, a typed value, plus. + The panel has no bare number box. A number box is hard to hit with a finger, + on a tablet or a phone, and gives no limit that you can see. */ +#sfzbuilder-library .sfz-step { + display: inline-flex; + border: 1px solid var(--sfz-line); + height: 26px; +} +#sfzbuilder-library .sfz-step button { + appearance: none; + background: transparent; + border: 0; + color: var(--sfz-dim); + width: 26px; + flex: 0 0 26px; + cursor: pointer; + font-size: 15px; + line-height: 1; + padding: 0; + transition: background var(--sfz-snap) linear, color var(--sfz-snap) linear; +} +#sfzbuilder-library .sfz-step button:hover { background: var(--sfz-accent); color: #fff; } +#sfzbuilder-library .sfz-step button:disabled { color: #565656; cursor: default; background: transparent; } +#sfzbuilder-library .sfz-step input { + /* Three digits and no more: the widest value the panel takes is 128 pads. + The tool row holds three of these fields side by side and wraps when the + centre column is narrow, so the width they do not need is worth having. */ + width: 38px; + height: 100%; + background: #000; + border: 0; + border-left: 1px solid var(--sfz-line); + border-right: 1px solid var(--sfz-line); + color: var(--sfz-text); + text-align: center; + font-size: 13px; + padding: 0; +} +#sfzbuilder-library .sfz-step input:focus { color: var(--sfz-accent-fg); } + +/* The small stepper of a pad. minmax(0,1fr) and width:0 keep it inside the pad + at six columns. A bare 1fr track has an automatic minimum of the content + width, which pushes the stepper past the edge. */ +#sfzbuilder-library .sfz-step--mini { height: 19px; width: 100%; min-width: 0; max-width: 100%; } +#sfzbuilder-library .sfz-step--mini button { width: 16px; flex: 0 0 16px; font-size: 11px; } +#sfzbuilder-library .sfz-step--mini input { flex: 1 1 0; width: 0; min-width: 0; font-size: 10px; padding: 0 2px; } + +/* The note token. The MIDI number and the note name have one appearance in + every place that shows them. */ +#sfzbuilder-library .sfz-note { display: inline-flex; align-items: baseline; gap: 7px; } +#sfzbuilder-library .sfz-note .sfz-note-num { color: var(--sfz-dim); font-size: 15px; } +#sfzbuilder-library .sfz-note .sfz-note-name { color: var(--sfz-accent-fg); font-size: 15px; font-weight: 700; } +#sfzbuilder-library .sfz-note--out .sfz-note-name { color: var(--sfz-err); } +#sfzbuilder-library .sfz-step .sfz-note { + height: 24px; + align-items: center; + cursor: text; + justify-content: center; +} + +/* The value of the note stepper keeps one width whatever it shows. + * + * The number goes from "0" to "127" and the name from "C2" to "A#-1", so the + * content is between four and seven characters wide. A box that sizes to its + * content thus moves the minus and the plus under the pointer while you step + * through the notes, and a click, which turns the box into a field that takes a + * number, changed the width a third time. + * + * Each part gets a column of its own, in ch, so the columns follow the font of + * the device instead of a measure taken here. The number is right aligned and + * the name left aligned, so neither one slides when the number gains a digit. + * --sfz-token-w is then a floor for the box, not the width itself: the content + * is already constant, and a min-width cannot clip a wide font the way a fixed + * width can. */ +#sfzbuilder-library .sfz-step .sfz-note-num, +#sfzbuilder-library .sfz-step .sfz-note-name { flex: 0 0 auto; display: inline-block; } +#sfzbuilder-library .sfz-step .sfz-note-num { width: 3ch; text-align: right; } +#sfzbuilder-library .sfz-step .sfz-note-name { width: 4ch; text-align: left; } + +#sfzbuilder-library .sfz-tokenbox { + display: inline-flex; + justify-content: center; + min-width: var(--sfz-token-w); + border-left: 1px solid var(--sfz-line); + border-right: 1px solid var(--sfz-line); + background: #000; +} +/* The field that a click opens fills the box, so the box keeps its width. */ +/* flex-basis 0 and width 0, as the small stepper of a pad does: a basis of + auto falls back to the intrinsic width of an input, which is about twenty + characters, and the box grew when you clicked it. */ +#sfzbuilder-library .sfz-step input.sfz-token-edit { + flex: 1 1 0; + width: 0; + min-width: 0; + border: 0; +} + +/* ---------- the bank rail ---------- */ + +#sfzbuilder-library .sfz-rail { + background: var(--sfz-rail); + border-right: 1px solid var(--sfz-line); + display: flex; + flex-direction: column; + min-height: 0; +} +#sfzbuilder-library .sfz-rail-head { + padding: 14px 16px; + border-bottom: 1px solid var(--sfz-line); + display: flex; + justify-content: space-between; + align-items: center; + flex: 0 0 auto; +} +#sfzbuilder-library .sfz-add { + appearance: none; + background: transparent; + border: 0; + padding: 0; + color: var(--sfz-accent-fg); + cursor: pointer; + font-size: 11px; + letter-spacing: .14em; + text-transform: uppercase; +} +#sfzbuilder-library .sfz-add:hover { text-decoration: underline; } +#sfzbuilder-library .sfz-bank-list { overflow-y: auto; flex: 1; min-height: 0; } +#sfzbuilder-library .sfz-bank { + padding: 11px 16px; + font-size: 14px; + cursor: pointer; + color: var(--sfz-dim); + border-left: 3px solid transparent; + display: flex; + justify-content: space-between; + gap: 8px; + transition: background var(--sfz-snap) linear, color var(--sfz-snap) linear; +} +#sfzbuilder-library .sfz-bank:hover { background: #000; color: var(--sfz-text); } +#sfzbuilder-library .sfz-bank-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +#sfzbuilder-library .sfz-bank-count { font-size: 12px; color: var(--sfz-dimmer); flex: 0 0 auto; } +#sfzbuilder-library .sfz-bank--on { background: var(--sfz-accent); color: #fff; border-left-color: #000; font-weight: 700; } +#sfzbuilder-library .sfz-bank--on .sfz-bank-count { color: #fff; opacity: .75; } +#sfzbuilder-library .sfz-new-bank { + width: 100%; + background: #000; + border: 1px solid var(--sfz-accent); + color: var(--sfz-text); + padding: 9px 14px; + font-size: 14px; +} + +/* ---------- the centre ---------- */ + +#sfzbuilder-library .sfz-center { display: flex; flex-direction: column; min-width: 0; min-height: 0; } +/* The head is a column with a gap, not a stack with a margin under the title + row. The tool row is hidden while no bank is open; a margin on the title row + is drawn whether or not anything follows it, and left a band of empty header + under the title in that state. A gap is drawn between two visible rows only. */ +#sfzbuilder-library .sfz-head { + padding: 14px 18px; + border-bottom: 1px solid var(--sfz-line); + flex: 0 0 auto; + display: flex; + flex-direction: column; + gap: 10px; +} +#sfzbuilder-library .sfz-head-top { display: flex; align-items: baseline; gap: 16px; } +#sfzbuilder-library .sfz-title { + margin: 0; + padding: 0; + font-size: 17px; + line-height: 1.4; + letter-spacing: .02em; + color: #fff; + font-weight: 700; + font-family: var(--sfz-mono) !important; + flex: 0 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +#sfzbuilder-library .sfz-head-right { margin-left: auto; display: flex; align-items: center; gap: 8px; flex: 0 0 auto; } +/* The field that renames the bank stands where the title stands, so the name + does not move when you open it. */ +#sfzbuilder-library .sfz-title-edit { + flex: 0 1 auto; + min-width: 0; + max-width: 320px; + background: #000; + border: 1px solid var(--sfz-accent); + color: #fff; + font-size: 17px; + font-weight: 700; + letter-spacing: .02em; + padding: 1px 6px; +} +#sfzbuilder-library .sfz-tools { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; } +#sfzbuilder-library .sfz-field { display: flex; gap: 6px; align-items: center; } +/* The mark that says the pads hold a change no save wrote. It reads as a word, + not as a dot: a dot beside a name means a different thing in every program + that draws one. */ +#sfzbuilder-library .sfz-dirty { + flex: 0 0 auto; + border: 1px solid var(--sfz-accent); + color: var(--sfz-accent); + font-size: 9px; + letter-spacing: .18em; + text-transform: uppercase; + font-weight: 700; + padding: 2px 7px; + align-self: center; +} + +/* The status takes what the title leaves and gives the rest back to the Save + button, so a long message can never push Save off the line. */ +#sfzbuilder-library .sfz-status { + flex: 1 1 auto; + min-width: 0; + color: var(--sfz-dim); + font-family: var(--sfz-sans) !important; + font-size: 13px; + letter-spacing: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +#sfzbuilder-library .sfz-status--err { color: var(--sfz-err); } + +/* min-height:0 is needed. A flex child that scrolls has an automatic minimum + height of its content. Without this it grows past the panel and draws over + the page behind it. min-width:0 is the same rule on the other axis, and it is + what lets the grid scroll sideways instead of pushing the panel wider. + + The column count is what you asked for and nothing else picks it: the grid + never re-flows your eight pads into six rows of four. A column stretches to + fill whatever space is going, down to --sfz-pad-min and no further, and once + the count no longer fits at that width the grid scrolls sideways. So the far + end of the range is a choice you can still see, not one the panel silently + refuses. auto-fill would have been the other reading of "min width", but it + takes the count away from you, which is the one thing the stepper is for. */ +#sfzbuilder-library .sfz-grid { + flex: 1; + min-height: 0; + min-width: 0; + overflow: auto; + padding: 14px 14px 18px; + display: grid; + gap: 6px; + grid-template-columns: repeat(var(--sfz-cols, 4), minmax(var(--sfz-pad-min), 1fr)); + grid-auto-rows: minmax(150px, auto); + align-content: start; +} + +/* ---------- a pad ---------- */ + +#sfzbuilder-library .sfz-pad { + background: var(--sfz-cell); + min-width: 0; + border: 1px solid var(--sfz-line-soft); + padding: 14px 16px; + cursor: pointer; + display: flex; + flex-direction: column; + gap: 10px; + transition: border-color var(--sfz-snap) linear, background var(--sfz-snap) linear; +} +#sfzbuilder-library .sfz-pad:hover { border-color: var(--sfz-dimmer); } + +/* Two columns. Column one holds the index on row one and the play control on + row two, so the play control uses the same space as the number. */ +#sfzbuilder-library .sfz-pad-head { + display: grid; + grid-template-columns: 20px minmax(0, 1fr); + column-gap: 8px; + row-gap: 3px; + align-items: baseline; +} +#sfzbuilder-library .sfz-pad-idx { grid-area: 1 / 1 / 2 / 2; color: var(--sfz-dim); font-size: 14px; } +#sfzbuilder-library .sfz-pad-play { + grid-area: 2 / 1 / 3 / 2; + appearance: none; + background: transparent; + border: 0; + padding: 0; + color: var(--sfz-dimmer); + font-size: 13px; + cursor: pointer; + text-align: left; + width: 20px; + transition: color var(--sfz-snap) linear; +} +#sfzbuilder-library .sfz-pad-play:hover, +#sfzbuilder-library .sfz-pad-play.sfz-playing { color: var(--sfz-accent-fg); } +#sfzbuilder-library .sfz-pad-meta { grid-area: 1 / 2 / 2 / 3; display: flex; align-items: baseline; gap: 10px; min-width: 0; } +#sfzbuilder-library .sfz-pad-clear { + appearance: none; + background: transparent; + border: 0; + padding: 0; + margin-left: auto; + color: var(--sfz-dimmer); + cursor: pointer; + font-size: 24px; + line-height: 12px; + transition: color var(--sfz-snap) linear; +} +#sfzbuilder-library .sfz-pad-clear:hover { color: var(--sfz-err); } +#sfzbuilder-library .sfz-pad-file { + grid-area: 2 / 2 / 3 / 3; + font-size: 14px; + min-width: 0; + color: #fff; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +#sfzbuilder-library .sfz-pad-free { + font-size: 10px; + color: var(--sfz-dimmer); + letter-spacing: .16em; + margin: auto; + text-align: center; +} + +/* One property on each line. This keeps a pad legible at six columns. */ +#sfzbuilder-library .sfz-props { display: grid; gap: 4px; margin-top: auto; min-width: 0; } +#sfzbuilder-library .sfz-prop { + display: grid; + grid-template-columns: 28px minmax(0, 1fr); + gap: 5px; + align-items: center; + min-width: 0; +} +#sfzbuilder-library .sfz-prop .sfz-lbl { font-size: 8px; } +#sfzbuilder-library .sfz-select { + width: 100%; + height: 19px; + font-size: 10px; + padding: 0 3px; + background: #000; + border: 1px solid var(--sfz-line); + color: var(--sfz-text); + transition: border-color var(--sfz-snap) linear; +} +#sfzbuilder-library .sfz-select:focus { border-color: var(--sfz-accent); } + +#sfzbuilder-library .sfz-pad--empty { border-style: solid; border-color: var(--sfz-line-free); background: transparent; } +#sfzbuilder-library .sfz-pad--filled { border-color: var(--sfz-line); } +/* The pad that is selected gets a ring, not an inversion. A pad holds form + controls, so the ground must stay dark to keep them legible. */ +#sfzbuilder-library .sfz-pad--on { + border-color: var(--sfz-accent); + box-shadow: inset 0 0 0 1px var(--sfz-accent); + background: rgba(136, 57, 150, .07); +} +/* While a drag runs, each pad says that it can take the sample. Without this + you cannot see where to put the sample until you are already over a pad. + jQuery UI adds --target to every pad and --drop to the one under the + pointer. */ +#sfzbuilder-library .sfz-pad--target { + border-color: rgba(136, 57, 150, .55); + border-style: dashed; + background: rgba(136, 57, 150, .04); +} +#sfzbuilder-library .sfz-pad--target .sfz-pad-free { color: var(--sfz-accent-fg); } +#sfzbuilder-library .sfz-pad--drop { + border-color: var(--sfz-accent); + border-style: solid; + box-shadow: inset 0 0 0 1px var(--sfz-accent); + background: rgba(136, 57, 150, .14); +} +#sfzbuilder-library .sfz-pad--drop .sfz-pad-free { color: var(--sfz-accent-fg); font-weight: 700; } +/* The row that you took the sample from. */ +#sfzbuilder-library .sfz-sample--dragging { background: #000; color: var(--sfz-dimmer); } + +/* ---------- no bank ---------- */ + +#sfzbuilder-library .sfz-empty-state { + flex: 1; + display: grid; + place-content: center; + text-align: center; + gap: 14px; + padding: 30px; +} +#sfzbuilder-library .sfz-empty-state h3 { + margin: 0; + font-size: 12px; + letter-spacing: .2em; + text-transform: uppercase; + color: var(--sfz-dim); + font-family: var(--sfz-mono) !important; +} +#sfzbuilder-library .sfz-gesture { + display: flex; + gap: 26px; + justify-content: center; + color: var(--sfz-dimmer); + font-size: 10px; + letter-spacing: .1em; +} +#sfzbuilder-library .sfz-gesture div { display: grid; gap: 7px; justify-items: center; } +#sfzbuilder-library .sfz-gesture .sfz-gesture-box { + width: 62px; + height: 42px; + border: 1px dashed #444; + display: grid; + place-items: center; + color: var(--sfz-accent-fg); + font-size: 16px; +} + +/* ---------- the sample list ---------- */ + +#sfzbuilder-library .sfz-samples { + border-left: 1px solid var(--sfz-line); + display: flex; + flex-direction: column; + background: var(--sfz-panel); + min-width: 0; + min-height: 0; +} +#sfzbuilder-library .sfz-samples-head { + padding: 14px 14px; + border-bottom: 1px solid var(--sfz-line); + display: grid; + gap: 8px; + flex: 0 0 auto; +} +#sfzbuilder-library .sfz-search { + background: #000; + border: 1px solid var(--sfz-line); + color: var(--sfz-text); + padding: 7px 10px; + font-size: 13px; + width: 100%; +} +#sfzbuilder-library .sfz-search:focus { border-color: var(--sfz-accent); } +#sfzbuilder-library .sfz-upload { display: block; margin: 0; padding: 7px 10px; font-size: 10px; } +/* display:none takes the file input out of the tab order, and the label around + it is not focusable either, so Upload could not be reached by the keyboard at + all. The input is instead put where it cannot be seen but keeps its focus, + and the label wears the ring for it. */ +#sfzbuilder-library .sfz-upload { position: relative; } +#sfzbuilder-library .sfz-upload input { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + border: 0; + clip-path: inset(50%); + overflow: hidden; + white-space: nowrap; +} +#sfzbuilder-library .sfz-upload:focus-within { + outline: 2px solid var(--sfz-focus) !important; + outline-offset: -2px; +} +#sfzbuilder-library .sfz-sample-list { + list-style: none; + margin: 0; + padding: 0; + overflow-y: auto; + flex: 1; + min-height: 0; +} +#sfzbuilder-library .sfz-sample { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 14px; + cursor: grab; + color: var(--sfz-dim); + transition: background var(--sfz-snap) linear, color var(--sfz-snap) linear; +} +#sfzbuilder-library .sfz-sample:hover { background: #000; color: var(--sfz-text); } +#sfzbuilder-library .sfz-sample-play { + appearance: none; + background: transparent; + border: 0; + padding: 0; + color: var(--sfz-dimmer); + width: 14px; + flex: 0 0 14px; + font-size: 12px; + cursor: pointer; +} +#sfzbuilder-library .sfz-sample:hover .sfz-sample-play, +#sfzbuilder-library .sfz-sample-play.sfz-playing { color: var(--sfz-accent-fg); } +#sfzbuilder-library .sfz-sample-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 14px; + font-family: var(--sfz-sans) !important; +} +#sfzbuilder-library .sfz-sample-size { + margin-left: auto; + font-size: 11px; + color: var(--sfz-dimmer); + flex: 0 0 auto; + padding-left: 6px; +} +#sfzbuilder-library .sfz-sample-none { padding: 14px; color: var(--sfz-dimmer); font-size: 12px; } + +/* The thing that you drag. + jQuery UI puts it on the body, outside the panel, so this rule cannot start + at #sfzbuilder-library. Each var() gives a value to fall back on, because + this element is the one that is furthest from the panel. */ +.sfz-drag-helper { + display: flex; + align-items: center; + gap: 8px; + max-width: 280px; + background: var(--sfz-accent, #883996); + color: #fff; + border: 1px solid #000; + box-shadow: 0 4px 14px rgba(0, 0, 0, .55); + font-family: var(--sfz-mono, Menlo, monospace) !important; + font-size: 12px; + font-weight: 700; + padding: 7px 12px; + pointer-events: none; + cursor: grabbing; + z-index: 10000; +} +.sfz-drag-helper * { font-family: var(--sfz-mono, Menlo, monospace) !important; } +.sfz-drag-helper .sfz-drag-mark { font-size: 13px; line-height: 1; flex: 0 0 auto; } +.sfz-drag-helper .sfz-drag-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: var(--sfz-sans, system-ui, sans-serif) !important; + font-weight: 600; +} + +/* ---------- the drop cover ---------- */ + +#sfzbuilder-library .sfz-overlay { + position: absolute; + inset: 0; + z-index: 20; + display: none; + background: rgba(0, 0, 0, .86); + border: 2px dashed var(--sfz-accent); + place-content: center; + text-align: center; + gap: 10px; + pointer-events: none; +} +#sfzbuilder-library .sfz-overlay--on { display: grid; } +#sfzbuilder-library .sfz-overlay-big { + font-size: 15px; + letter-spacing: .24em; + text-transform: uppercase; + color: var(--sfz-accent-fg); + font-weight: 700; +} +#sfzbuilder-library .sfz-overlay-sub { font-size: 10px; letter-spacing: .12em; color: var(--sfz-dim); } diff --git a/html/js/app/sfzbuilder/state.js b/html/js/app/sfzbuilder/state.js new file mode 100644 index 00000000..1efbb19d --- /dev/null +++ b/html/js/app/sfzbuilder/state.js @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later +// @ts-check + +/** + * The state of the SFZ builder. + * The view is a function of this state. + * Only `update` changes the state. Thus there is one place to write log messages. + * @module + */ +import { DEFAULT_PADS } from './model.js' + +/** + * @typedef {object} SfzState + * @property {string[]} banks + * @property {Record} bankCounts How many samples each bank holds. + * @property {string} currentBank + * @property {import('./model.js').SampleFile[]} bankFiles The files of the current bank. + * @property {import('./model.js').SampleFile[]} files The files that the list shows. + * @property {(import('./model.js').Slot|null)[]} slots + * @property {number} selectedSlot The index of the selected pad, or -1. + * @property {number} baseNote + * @property {number} padCount + * @property {number} cols How many pads on each line. + * @property {string} source + * @property {string} search + * @property {string} status + * @property {boolean} statusIsError + * @property {boolean} dirty Whether the pads hold a change that no save wrote. + */ + +/** + * Makes the first state. + * @returns {SfzState} + */ +export function initialState() { + return { + banks: [], + bankCounts: {}, + currentBank: '', + bankFiles: [], + files: [], + slots: [], + selectedSlot: -1, + baseNote: 36, + padCount: DEFAULT_PADS, + cols: 4, + source: 'bank', + search: '', + status: '', + statusIsError: false, + dirty: false, + } +} + +/** + * Makes a store that holds the state. + * @param {(state: SfzState) => void} onChange Runs after each change. + */ +export function createStore(onChange) { + let state = initialState() + return { + /** @returns {SfzState} */ + get() { + return state + }, + /** + * Changes the state and then draws the panel. + * @param {Partial} patch + */ + update(patch) { + state = Object.assign({}, state, patch) + onChange(state) + }, + } +} diff --git a/html/js/app/sfzbuilder/view.js b/html/js/app/sfzbuilder/view.js new file mode 100644 index 00000000..0d792f8b --- /dev/null +++ b/html/js/app/sfzbuilder/view.js @@ -0,0 +1,728 @@ +// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later +// @ts-check + +/** + * The markup and the drawing code of the SFZ builder. + * + * This file makes every element of the panel (architecture rule 4.3). + * index.html holds an empty container only. Thus the full feature is in one + * directory and the legacy markup does not limit the design. + * + * The file uses jQuery for one thing only: the drag of a sample onto a pad. + * jQuery UI gives `draggable` and `droppable`, which the old panel used and + * which work with a touch screen through the same code path. + * @module + */ + +import { jq } from './legacy.js' +import { h, clear, toggleClass, show, clickable } from './dom.js' +import * as model from './model.js' + +/** What the title says when no bank is open. */ +const NO_BANK_TITLE = 'No bank' + +/** + * Makes a stepper: a minus button, a value and a plus button. + * + * The panel has no bare number box. A number box is hard to hit with a finger, + * on a tablet or a phone, and shows no limit. + * + * A stepper can hold no value when `nullable` is true. The gain and the root + * note of a pad have no value by default, which the SFZ file needs: a pad with + * no root note plays its sample at the written speed. The box is then empty and + * shows the placeholder. A minus or a plus starts from `spec.start`. + * + * @param {object} spec + * @param {number} spec.min + * @param {number} spec.max + * @param {number|null} spec.value + * @param {(value: number|null) => void} spec.onSet Runs after each change. + * @param {boolean} [spec.token] True draws the value as a note token. + * @param {boolean} [spec.nullable] True lets the value be empty. + * @param {number} [spec.start] The value that a click gives to an empty box. + * @param {string} [spec.className] + * @param {string} [spec.placeholder] + * @param {string} [spec.title] + * @returns {{ node: HTMLElement, set: (value: number|null) => void, get: () => number|null }} + */ +export function makeStepper(spec) { + /** @type {number|null} */ + let value = spec.value + /** @type {HTMLInputElement|null} */ + let input = null + /** @type {HTMLElement} */ + let box + + const minus = h('button', { type: 'button', html: '−', title: 'Less' }) + const plus = h('button', { type: 'button', text: '+', title: 'More' }) + + if (spec.token) { + // The box is a span, so it needs the tab order and the keys of a button + // given to it. This happens one time here and not in drawToken, which + // runs again after every change and would else add a listener each time. + box = clickable(h('span.sfz-tokenbox'), openEditor) + } else { + input = /** @type {HTMLInputElement} */ (h('input', { + type: 'text', + inputmode: 'numeric', + value: value === null ? '' : String(value), + placeholder: spec.placeholder || '', + })) + box = input + } + + const node = h('span.sfz-step' + (spec.className ? '.' + spec.className : ''), + { title: spec.title || '' }, [minus, box, plus]) + + /** @param {number|null} next */ + function set(next) { + if (next === null || isNaN(next)) { + value = spec.nullable ? null : spec.min + } else { + value = Math.max(spec.min, Math.min(spec.max, next)) + } + draw() + spec.onSet(value) + } + + /** Gives the value that a minus or a plus starts from. */ + function base() { + if (value !== null) { + return value + } + const start = typeof spec.start === 'number' ? spec.start : spec.min + return Math.max(spec.min, Math.min(spec.max, start)) + } + + function draw() { + if (input) { + input.value = value === null ? '' : String(value) + } else { + drawToken() + } + // @ts-ignore - a button has `disabled`. + minus.disabled = value !== null && value <= spec.min + // @ts-ignore + plus.disabled = value !== null && value >= spec.max + } + + // The token box shows "36 C2". A click turns it into a box that takes a + // number, so you can go to a far note without many clicks. + function drawToken() { + clear(box) + // A button again: the box holds text only in this condition. + box.setAttribute('role', 'button') + box.appendChild(noteTokenNode(value === null ? spec.min : value)) + } + + /** Turns the token box into the field that takes a number. */ + function openEditor() { + const edit = /** @type {HTMLInputElement} */ (h('input.sfz-token-edit', { + type: 'text', + inputmode: 'numeric', + value: String(value), + })) + let done = false + function commit() { + if (done) { + return + } + done = true + set(parseInt(edit.value, 10)) + } + clear(box) + // The box now holds a field that takes a number. The children of a + // button are presentational, so the role goes away while the field is + // open, or a screen reader could say nothing of what you type. + box.removeAttribute('role') + box.appendChild(edit) + edit.focus() + edit.select() + edit.addEventListener('blur', commit) + edit.addEventListener('keydown', (/** @type {any} */ e) => { + if (e.key === 'Enter') { + commit() + } else if (e.key === 'Escape') { + done = true + drawToken() + box.focus() + } + }) + } + + minus.addEventListener('click', () => set(value === null ? base() : value - 1)) + plus.addEventListener('click', () => set(value === null ? base() : value + 1)) + if (input) { + // An empty box means "no value". Thus you can go back to the automatic + // condition after you give a number. + const read = () => { + const text = (input ? input.value : '').trim() + return text === '' ? null : parseInt(text, 10) + } + input.addEventListener('change', () => set(read())) + input.addEventListener('keydown', (/** @type {any} */ e) => { + if (e.key === 'Enter') { + set(read()) + } + }) + } + + draw() + return { node: node, set: set, get: () => value } +} + +/** + * Makes the element that shows a MIDI note. + * The panel shows a note in three places. All three use this element, so the + * idea has one appearance. + * @param {number} note + * @returns {HTMLElement} + */ +export function noteTokenNode(note) { + const token = model.noteToken(note) + return h('span.sfz-note' + (token.inRange ? '' : '.sfz-note--out'), {}, [ + h('span.sfz-note-num', { text: token.inRange ? String(note) : '--' }), + h('span.sfz-note-name', { text: token.name }), + ]) +} + +/** + * Makes the full panel inside the container. + * + * @param {HTMLElement} root The element of the panel, `#sfzbuilder-library`. + * @param {object} handlers + * @param {() => void} handlers.onAddBank + * @param {(source: string) => void} handlers.onSource + * @param {() => void} handlers.onUpload + * @param {(term: string) => void} handlers.onSearch + * @param {(count: number) => void} handlers.onPadCount + * @param {(cols: number) => void} handlers.onCols + * @param {(note: number) => void} handlers.onBaseNote + * @param {() => void} handlers.onSave + * @param {() => void} handlers.onRenameBank + * @param {() => void} handlers.onDeleteBank + */ +export function createLayout(root, handlers) { + clear(root) + + const bankList = h('div.sfz-bank-list') + const addBank = h('button.sfz-add', { type: 'button', text: '+ New', onclick: handlers.onAddBank }) + const rail = h('div.sfz-rail', {}, [ + h('div.sfz-rail-head', {}, [h('span.sfz-lbl', { text: 'Banks' }), addBank]), + bankList, + ]) + + // "Untitled" said that the bank had no name. A bank always has a name; what + // the panel means here is that no bank is open, which is a different idea. + const title = h('h2.sfz-title', { text: NO_BANK_TITLE }) + const saveButton = h('button.sfz-btn.sfz-save', { type: 'button', text: 'Save', title: 'Write the SFZ file of this bank' , onclick: handlers.onSave }) + const renameButton = h('button.sfz-btn.sfz-btn--quiet', { + type: 'button', text: 'Rename', title: 'Change the name of this bank', + onclick: handlers.onRenameBank, + }) + const deleteButton = h('button.sfz-btn.sfz-btn--quiet.sfz-btn--danger', { + type: 'button', text: 'Delete', title: 'Remove this bank and every file in it', + onclick: handlers.onDeleteBank, + }) + // The mark that says the pads hold a change no save wrote. It is a sibling + // of the title and not a character added to it, so the name of the bank + // stays the text of the title alone -- the ellipsis of a long name would + // else eat the mark, and a screen reader would read it as part of the name. + const dirtyMark = h('span.sfz-dirty', { text: 'Unsaved', hidden: true }) + const status = h('span.sfz-status') + + const padStepper = makeStepper({ + min: model.MIN_PADS, max: model.MAX_PADS, value: model.DEFAULT_PADS, + title: 'How many pads this bank has', + // A stepper that is not nullable never gives null. The clamp says so + // to the type checker and costs nothing. + onSet: (v) => handlers.onPadCount(model.clampPadCount(v)), + }) + const colStepper = makeStepper({ + min: model.MIN_COLS, max: model.MAX_COLS, value: model.DEFAULT_COLS, + title: 'How many pads on each line', + onSet: (v) => handlers.onCols(model.clampCols(v)), + }) + const baseStepper = makeStepper({ + min: 0, max: model.MAX_NOTE, value: model.DEFAULT_BASE_NOTE, token: true, + title: 'The MIDI note of the first pad', + onSet: (v) => handlers.onBaseNote(model.clampBaseNote(v)), + }) + + const tools = h('div.sfz-tools', {}, [ + h('span.sfz-field', {}, [h('span.sfz-lbl', { text: 'Pads' }), padStepper.node]), + h('span.sfz-field', {}, [h('span.sfz-lbl', { text: 'Cols' }), colStepper.node]), + h('span.sfz-field', {}, [h('span.sfz-lbl', { text: 'Base' }), baseStepper.node]), + ]) + + const grid = h('div.sfz-grid') + const emptyState = h('div.sfz-empty-state', {}, [ + h('h3', { text: 'No sound bank selected' }), + h('div.sfz-gesture', {}, [ + h('div', {}, [h('span.sfz-gesture-box', { text: '+' }), h('span', { text: 'New bank' })]), + h('div', {}, [h('span.sfz-gesture-box', { html: '⇩' }), h('span', { text: 'Drop audio' })]), + h('div', {}, [h('span.sfz-gesture-box', { html: '▶' }), h('span', { text: 'Drag to a pad' })]), + ]), + ]) + + const center = h('div.sfz-center', {}, [ + // The status sits on the title line, not in the tool row. The tool row + // is hidden while no bank is open, and that is exactly when the panel + // has the most to say ("Select a bank first"); the head is never + // hidden. In the tool row it also wrapped onto a line of its own as + // soon as the centre column went under about 490px, which read as an + // empty line under the title. + h('div.sfz-head', {}, [ + h('div.sfz-head-top', {}, [title, dirtyMark, status, + h('div.sfz-head-right', {}, [renameButton, deleteButton, saveButton])]), + tools, + ]), + grid, + emptyState, + ]) + + /** @type {HTMLElement[]} */ + const sourceButtons = [] + const sourceBar = h('div.sfz-seg') + for (const entry of [['bank', 'Bank'], ['device', 'Device'], ['usb', 'USB']]) { + const button = h('button', { + type: 'button', + text: entry[1], + 'data-source': entry[0], + 'aria-pressed': entry[0] === 'bank' ? 'true' : 'false', + onclick: () => handlers.onSource(entry[0]), + }) + sourceButtons.push(button) + sourceBar.appendChild(button) + } + + const uploadInput = /** @type {HTMLInputElement} */ (h('input', { + type: 'file', multiple: true, accept: 'audio/*', onchange: handlers.onUpload, + })) + const search = /** @type {HTMLInputElement} */ (h('input.sfz-search', { + type: 'search', placeholder: 'FILTER…', autocomplete: 'off', + oninput: () => handlers.onSearch(search.value || ''), + })) + const sampleList = h('ul.sfz-sample-list') + + const samples = h('div.sfz-samples', {}, [ + h('div.sfz-samples-head', {}, [ + h('span.sfz-lbl', { text: 'Samples' }), + sourceBar, + h('label.sfz-btn.sfz-upload', {}, ['Upload', uploadInput]), + search, + ]), + sampleList, + ]) + + const overlaySub = h('div.sfz-overlay-sub') + const overlay = h('div.sfz-overlay', {}, [ + h('div.sfz-overlay-big', { text: 'Drop audio files' }), + overlaySub, + ]) + + // The shared header bar of the other panels. main.less draws it, so the + // classes and the structure must be the same as the other panels. The one + // difference is in main.less: this panel does not draw the MOD icon. + const header = h('header', {}, [ + h('h1.bottom.top', { text: 'Sound Bank Builder' }), + ]) + + const panel = h('div.sfz-panel', {}, [rail, center, samples, overlay]) + root.appendChild(header) + root.appendChild(panel) + + return { + root, header, panel, rail, bankList, addBank, + title, saveButton, renameButton, deleteButton, tools, status, dirtyMark, + grid, emptyState, + sourceButtons, uploadInput, search, sampleList, + overlay, overlaySub, + padStepper, colStepper, baseStepper, + } +} + +/** @typedef {ReturnType} Elements */ + +/** + * Shows the status message. + * @param {Elements} el + * @param {string} message + * @param {boolean} isError + */ +export function renderStatus(el, message, isError) { + el.status.textContent = message || '' + toggleClass(el.status, 'sfz-status--err', isError) +} + +/** + * Keeps the title, the tools and the Save button in step with the bank. + * @param {Elements} el + * @param {import('./state.js').SfzState} state + */ +export function renderBankView(el, state) { + const bank = state.currentBank + el.title.textContent = bank || NO_BANK_TITLE + show(el.emptyState, !bank) + show(el.grid, !!bank) + show(el.tools, !!bank) + el.dirtyMark.hidden = !bank || !state.dirty + for (const button of [el.saveButton, el.renameButton, el.deleteButton]) { + // @ts-ignore - a button has `disabled`. + button.disabled = !bank + } + el.overlaySub.textContent = bank + ? 'WAV · FLAC · AIFF · OGG · MP3 — added to ' + bank + : 'Select a bank first' + el.grid.style.setProperty('--sfz-cols', String(state.cols)) + for (const button of el.sourceButtons) { + button.setAttribute('aria-pressed', button.getAttribute('data-source') === state.source ? 'true' : 'false') + } +} + +/** + * Draws the list of the banks. + * @param {Elements} el + * @param {import('./state.js').SfzState} state + * @param {(name: string) => void} onSelect + */ +export function renderBanks(el, state, onSelect) { + clear(el.bankList) + for (const name of state.banks) { + const row = clickable(h('div.sfz-bank' + (name === state.currentBank ? '.sfz-bank--on' : ''), { + title: name, + 'aria-pressed': name === state.currentBank ? 'true' : 'false', + }, [ + h('span.sfz-bank-name', { text: name }), + h('span.sfz-bank-count', { text: countLabel(state.bankCounts[name]) }), + ]), () => onSelect(name)) + el.bankList.appendChild(row) + } +} + +/** + * @param {number|undefined} count + * @returns {string} + */ +function countLabel(count) { + return typeof count === 'number' ? String(count) : '' +} + +/** + * Turns the title into a field that takes a new name for the bank. + * + * The rename happens where the name is, the same way a new bank is named in + * the place the row will take. The panel opens no dialog of its own: the one + * dialog it uses is the browser confirm before a delete, which is what the + * legacy code does for a change that cannot be undone. + * @param {Elements} el + * @param {(name: string) => void} onCommit + */ +export function promptRenameBank(el, onCommit) { + const current = el.title.textContent || '' + if (el.title.parentNode === null || el.title.hidden) { + return + } + const input = /** @type {HTMLInputElement} */ (h('input.sfz-title-edit', { + type: 'text', value: current, 'aria-label': 'The name of this bank', + })) + el.title.hidden = true + el.title.parentNode.insertBefore(input, el.title) + input.focus() + input.select() + + let done = false + /** @param {boolean} back Whether the focus goes to the Rename button. */ + function close(back) { + done = true + el.title.hidden = false + if (input.parentNode) { + input.parentNode.removeChild(input) + } + if (back) { + el.renameButton.focus() + } + } + input.addEventListener('keydown', (/** @type {any} */ e) => { + if (e.key === 'Enter') { + const name = (input.value || '').trim() + close(true) + if (name && name !== current) { + onCommit(name) + } + } else if (e.key === 'Escape') { + // A stray Escape must not reach the window manager and close the + // panel with the field still open. + e.stopPropagation() + close(true) + } + }) + input.addEventListener('blur', () => { + if (!done) { + close(false) + } + }) +} + +/** + * Opens the box that takes the name of a new bank. + * @param {Elements} el + * @param {(name: string) => void} onCommit + */ +export function promptNewBank(el, onCommit) { + if (el.bankList.querySelector('.sfz-new-bank')) { + return + } + const input = /** @type {HTMLInputElement} */ (h('input.sfz-new-bank', { + type: 'text', placeholder: 'Bank name', + })) + el.bankList.insertBefore(input, el.bankList.firstChild) + input.focus() + let done = false + function close() { + if (input.parentNode) { + input.parentNode.removeChild(input) + } + } + input.addEventListener('keydown', (/** @type {any} */ e) => { + if (e.key === 'Enter') { + done = true + const name = (input.value || '').trim() + close() + onCommit(name) + } else if (e.key === 'Escape') { + done = true + close() + } + }) + input.addEventListener('blur', () => { + if (!done) { + close() + } + }) +} + +/** + * Draws the list of the samples. + * @param {Elements} el + * @param {import('./state.js').SfzState} state + * @param {{ onPlay: (button: HTMLElement, fullname: string) => void, onPick: (file: import('./model.js').SampleFile) => void }} handlers + */ +export function renderSamples(el, state, handlers) { + const $ = jq() + const files = model.filterFiles(state.files, state.search) + clear(el.sampleList) + + if (files.length === 0) { + el.sampleList.appendChild(h('li.sfz-sample-none', { + text: state.search ? 'No sample has that name' : 'No sample here', + })) + return + } + + for (const f of files) { + const play = h('button.sfz-sample-play', { + type: 'button', + html: '▶', + title: 'Play this sample', + onclick: (/** @type {Event} */ e) => { + e.stopPropagation() + handlers.onPlay(play, f.fullname) + }, + }) + // A click fills the pad that is selected. This is the path for a touch + // screen, where a drag is awkward, and for the keyboard. + // + // The row holds a play button, so it keeps the listitem role that a li + // inside a ul already has. A button role would let a screen reader drop + // that play button. + const row = clickable(h('li.sfz-sample', { + title: f.fullname, + 'aria-label': f.basename + ', put on the pad that is selected', + }, [ + play, + h('span.sfz-sample-name', { text: f.basename }), + h('span.sfz-sample-size', { text: model.formatSize(f.size) }), + ]), () => handlers.onPick(f), null) + + const $row = $(row) + $row.data('sfz-file', f) + $row.draggable({ + revert: 'invalid', + // The helper goes on the body so that the panel, which hides what + // goes past its edge, does not cut it. + appendTo: 'body', + helper: () => $(h('div.sfz-drag-helper', {}, [ + h('span.sfz-drag-mark', { html: '▶' }), + h('span.sfz-drag-name', { text: f.basename }), + ])), + // The pointer holds the helper near its left edge, so the name is + // beside the pointer and the pad below stays visible. + cursorAt: { left: 14, top: 14 }, + // A click puts the sample on the pad that is selected. A drag needs + // a longer movement, so a small movement does not eat the click. + distance: 6, + zIndex: 10000, + start: () => row.classList.add('sfz-sample--dragging'), + stop: () => row.classList.remove('sfz-sample--dragging'), + }) + + el.sampleList.appendChild(row) + } +} + +/** + * Draws the pads. + * + * The gain, root and loop controls send one field each to `onEdit`. They do not + * draw the pads again, so the focus stays in the box while you type. + * + * @param {Elements} el + * @param {import('./state.js').SfzState} state + * @param {object} handlers + * @param {(index: number, file: import('./model.js').SampleFile) => void} handlers.onAssign + * @param {(index: number) => void} handlers.onSelect + * @param {(index: number) => void} handlers.onClear + * @param {(button: HTMLElement, fullname: string) => void} handlers.onPlay + * @param {(slot: import('./model.js').Slot) => string} handlers.previewUrl + * @param {(index: number, patch: Partial) => void} handlers.onEdit + */ +export function renderSlots(el, state, handlers) { + const $ = jq() + clear(el.grid) + el.grid.style.setProperty('--sfz-cols', String(state.cols)) + + state.slots.forEach((slot, idx) => { + const note = model.padNote(state.baseNote, idx) + + const head = h('div.sfz-pad-head', {}, [ + h('span.sfz-pad-idx', { text: padIndex(idx) }), + h('div.sfz-pad-meta', {}, [ + noteTokenNode(note), + slot ? h('button.sfz-pad-clear', { + type: 'button', + html: '×', + title: 'Clear this pad', + onclick: (/** @type {Event} */ e) => { + e.stopPropagation() + handlers.onClear(idx) + }, + }) : null, + ]), + ]) + + if (slot) { + const preview = handlers.previewUrl(slot) + const play = h('button.sfz-pad-play', { + type: 'button', + html: '▶', + title: preview ? 'Play this sample' : 'This sample is not here yet', + disabled: !preview, + onclick: (/** @type {Event} */ e) => { + e.stopPropagation() + if (preview) { + handlers.onPlay(play, preview) + } + }, + }) + head.appendChild(play) + head.appendChild(h('div.sfz-pad-file', { text: slot.sample, title: slot.sample })) + } + + // A pad is a group, not a button. It holds the clear and play buttons, + // two steppers and the loop menu, and the children of a button are + // presentational: a screen reader may drop every one of them. A group + // keeps its children. aria-pressed belongs to a button, so the + // selection is said in the label instead. + const pad = clickable(h('div.sfz-pad' + + (slot ? '.sfz-pad--filled' : '.sfz-pad--empty') + + (idx === state.selectedSlot ? '.sfz-pad--on' : ''), + { + 'aria-label': 'Pad ' + (idx + 1) + ', note ' + model.noteToken(note).name + + (slot ? ', ' + slot.sample : ', free') + + (idx === state.selectedSlot ? ', selected' : ''), + }, + [head, slot ? padProps(slot, idx, handlers.onEdit) : h('div.sfz-pad-free', { text: '— FREE —' })]), + () => handlers.onSelect(idx), 'group') + + // Each pad takes its own drop, so a sample lands where you put it. + $(pad).droppable({ + accept: '.sfz-sample', + // --target marks every pad while a drag runs, so you can see where + // a sample can go. --drop marks the pad under the pointer. + activeClass: 'sfz-pad--target', + hoverClass: 'sfz-pad--drop', + tolerance: 'pointer', + drop: (/** @type {any} */ _e, /** @type {any} */ ui) => { + const entry = ui.draggable.data('sfz-file') + if (entry) { + handlers.onAssign(idx, entry) + } + }, + }) + + el.grid.appendChild(pad) + }) +} + +/** + * @param {number} idx + * @returns {string} + */ +function padIndex(idx) { + const n = idx + 1 + return n < 10 ? '0' + n : String(n) +} + +/** + * Makes the gain, root and loop controls of one pad. + * One control on each line. This keeps a pad legible at eight columns. + * + * A control sends the index of its pad and the one field it changed, never the + * pad object it was drawn from. The pads are not drawn again while you edit -- + * the focus has to stay in the box you are typing in -- so a control that held + * the object would go on writing into a pad that the store had already + * replaced. + * @param {import('./model.js').Slot} slot The values the controls open with. + * @param {number} idx + * @param {(index: number, patch: Partial) => void} onEdit + * @returns {HTMLElement} + */ +function padProps(slot, idx, onEdit) { + const gain = makeStepper({ + min: -144, max: 48, value: typeof slot.volume === 'number' ? slot.volume : null, + nullable: true, start: 0, placeholder: '0', + className: 'sfz-step--mini', + title: 'Gain in dB. An empty box means no change.', + onSet: (v) => onEdit(idx, { volume: v }), + }) + + // The root note has no value by default. sfizz then plays the sample at the + // written speed, which is correct for a drum hit. An empty box keeps that. + const root = makeStepper({ + min: 0, max: model.MAX_NOTE, + value: typeof slot.pitch === 'number' ? slot.pitch : null, + nullable: true, start: model.DEFAULT_BASE_NOTE, placeholder: 'AUTO', + className: 'sfz-step--mini', + title: 'The note that plays the sample at the written speed. Empty is automatic.', + onSet: (v) => onEdit(idx, { pitch: v }), + }) + + const loop = /** @type {HTMLSelectElement} */ (h('select.sfz-select', { + title: 'What happens when you release the pad', + onchange: () => onEdit(idx, { loop: loop.value }), + }, [ + h('option', { value: 'one_shot', text: 'ONE_SHOT' }), + h('option', { value: 'no_loop', text: 'NO_LOOP' }), + ])) + loop.value = slot.loop + + const props = h('div.sfz-props', {}, [ + h('span.sfz-prop', {}, [h('span.sfz-lbl', { text: 'Gain' }), gain.node]), + h('span.sfz-prop', {}, [h('span.sfz-lbl', { text: 'Root' }), root.node]), + loop, + ]) + props.addEventListener('click', (e) => e.stopPropagation()) + return props +} diff --git a/html/js/desktop.js b/html/js/desktop.js index 1c3b1e6e..198e86a6 100644 --- a/html/js/desktop.js +++ b/html/js/desktop.js @@ -39,6 +39,8 @@ function Desktop(elements) { pedalboardTrigger: $('
'), fileManagerBox: $('
'), fileManagerBoxTrigger: $('
'), + sfzBuilderBox: $('
'), + sfzBuilderBoxTrigger: $('
'), pedalboardBox: $('
'), pedalboardBoxTrigger: $('
'), bankBox: $('
'), @@ -736,6 +738,8 @@ function Desktop(elements) { elements.bankBoxTrigger) this.fileManagerBox = self.makeFileManagerBox(elements.fileManagerBox, elements.fileManagerBoxTrigger) + this.sfzBuilderBox = self.makeSfzBuilderBox(elements.sfzBuilderBox, + elements.sfzBuilderBoxTrigger) this.getPluginsData = function (uris, callback) { $.ajax({ @@ -1296,6 +1300,7 @@ function Desktop(elements) { elements.cloudPluginBoxTrigger.statusTooltip() elements.patchstorageBoxTrigger.statusTooltip() elements.fileManagerBoxTrigger.statusTooltip() + elements.sfzBuilderBoxTrigger.statusTooltip() this.upgradeWindow = elements.upgradeWindow.upgradeWindow({ icon: elements.upgradeIcon, @@ -1831,6 +1836,13 @@ Desktop.prototype.makeFileManagerBox = function (el, trigger) { }) } +Desktop.prototype.makeSfzBuilderBox = function (el, trigger) { + return el.sfzBuilderBox({ + 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/pedalboard.js b/html/js/pedalboard.js index c87da21f..be19e93b 100644 --- a/html/js/pedalboard.js +++ b/html/js/pedalboard.js @@ -62,7 +62,11 @@ $.ui.intersect = function(draggable, droppable, toleranceMode) { case "pointer": draggableLeft = ((draggable.positionAbs || draggable.position.absolute).left + (draggable.clickOffset || draggable.offset.click).left); draggableTop = ((draggable.positionAbs || draggable.position.absolute).top + (draggable.clickOffset || draggable.offset.click).top); - return isOverAxis( draggableTop, t, droppable.proportions().height ) && isOverAxis( draggableLeft, l, droppable.proportions().width ); + // isOverAxis() is private to the jQuery UI closure, and proportions is a + // property here and not a function. Both are inlined below. + // isOverAxis(x, ref, size) is (x >= ref && x < ref + size), and b/r above + // are already t + height and l + width. + return (draggableTop >= t && draggableTop < b) && (draggableLeft >= l && draggableLeft < r); case "touch": return ( (y1 >= t && y1 <= b) || // Top edge touching diff --git a/html/js/sfzbuilder.js b/html/js/sfzbuilder.js new file mode 100644 index 00000000..99173dcf --- /dev/null +++ b/html/js/sfzbuilder.js @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +/* + * The seam between the legacy code and the SFZ builder island. + * + * This file is a classic script. `desktop.js` calls `sfzBuilderBox` and the + * window manager needs the registration at once. Thus this file calls + * `self.window(options)` without a wait, and loads the island in the + * background. The panel opens later, so the island is ready in time. + * + * See docs/frontend-architecture.md, rule 4.2. + */ + +/* + * Writes why the panel is empty. + * + * index.html holds an empty container, because the island makes every element. + * Thus a file that the server does not give makes an empty panel and no other + * sign. The message below uses inline style, because the stylesheet of the + * island can be the file that is absent. + */ +function sfzBuilderShowLoadError(root, err) { + var text = (err && (err.message || err)) || 'unknown error' + root.innerHTML = '' + var box = document.createElement('div') + box.setAttribute('style', [ + 'position:absolute', 'top:0', 'bottom:45px', 'left:0', 'right:0', + 'z-index:2', 'background:#0a0a0a', 'color:#d8d8d8', 'padding:40px', + 'font-family:ui-monospace,Menlo,Consolas,monospace', 'font-size:13px', + 'line-height:1.7' + ].join(';')) + box.innerHTML = + '
Sound Bank Builder did not load
' + + '
The panel could not read its own files from' + + ' js/app/sfzbuilder/.' + + ' Look in the network tab of the browser for a 404.
' + + '
Reason:' + + '
' + box.querySelector('span').textContent = text + root.appendChild(box) +} + +JqueryClass('sfzBuilderBox', { + init: function (options) { + var self = $(this) + + options = $.extend({ + isMainWindow: true, + windowName: "Sound Bank Builder" + }, options) + + var root = self[0] + var island = import('./app/sfzbuilder/mount.js').then(function (module) { + return module.mount(root) + }) + island.catch(function (err) { + console.error('Cannot load the Sound Bank Builder', err) + sfzBuilderShowLoadError(root, err) + }) + self.data('sfzIsland', island) + + options.open = function () { + island.then(function (it) { it.open() }, function () { /* the message is on the panel */ }) + } + options.close = function () { + island.then(function (it) { it.close() }, function () { }) + } + + self.window(options) + + return self + } +}) diff --git a/mod/sfzbuilder.py b/mod/sfzbuilder.py new file mode 100755 index 00000000..8763edbd --- /dev/null +++ b/mod/sfzbuilder.py @@ -0,0 +1,420 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +import os +import re +import shutil +import json + +from mod import TextFileFlusher, safe_json_load +from mod.settings import USER_FILES_DIR + +# Audio extensions accepted as samples. Same set as FilesList.complete_audiofile_exts. +AUDIO_EXTENSIONS = ( + # through libsndfile + ".aif", ".aifc", ".aiff", ".au", ".bwf", ".flac", ".htk", ".iff", ".mat4", ".mat5", ".oga", ".ogg", ".opus", + ".paf", ".pvf", ".pvf5", ".sd2", ".sf", ".snd", ".svx", ".vcc", ".w64", ".wav", ".xi", + # extra through ffmpeg + ".3g2", ".3gp", ".aac", ".ac3", ".amr", ".ape", ".mp2", ".mp3", ".mpc", ".wma", +) + +LOOP_MODES = ("one_shot", "no_loop") + +# The sidecar is internal and never shown, so it keeps one name. The SFZ file is +# shown: the file selector of a plugin lists it by its basename alone, so every +# bank writing "bank.sfz" gave a list of identical rows and you could not tell +# which bank you were choosing. The SFZ file thus carries the name of its bank. +SIDECAR_NAME = "bank.json" +MAX_NOTES = 128 + + +def sfz_instruments_dir(): + return os.path.join(USER_FILES_DIR, "SFZ Instruments") + + +def is_audio_file(path): + return os.path.isfile(path) and path.lower().endswith(AUDIO_EXTENSIONS) + + +def sanitize_filename(name): + # Keep only safe characters. A space is one of them: generate_sfz puts + # sample= last on its line, where the value runs to the end of the line, so + # a space in the name cannot be read as the start of another opcode. + # The characters = < # are still removed: they break the SFZ parser. + name = os.path.basename(name or "") + stem, ext = os.path.splitext(name) + if not ext and name.startswith("."): + # Name like ".wav": treat the whole name as the extension. + stem, ext = "sample", name + stem = re.sub(r"[^A-Za-z0-9._ -]+", "_", stem).strip("._ ") + ext = re.sub(r"[^A-Za-z0-9.]", "", ext) + if not stem: + stem = "sample" + return stem + ext + + +def sanitize_bank_name(name): + if not name: + raise ValueError("bank name is empty") + name = os.path.basename(name) + name = re.sub(r"[^A-Za-z0-9._-]+", "_", name).strip(".") + if len(name) == 0: + raise ValueError("bank name is empty") + if name == "." or name == "..": + raise ValueError("invalid bank name") + return name + + +def unique_filename(name, used): + if name not in used: + return name + stem, ext = os.path.splitext(name) + i = 1 + while True: + candidate = "%s_%d%s" % (stem, i, ext) + if candidate not in used: + return candidate + i += 1 + + +def list_audio_files(root, exclude=None): + if not os.path.isdir(root): + return [] + excludepath = os.path.realpath(exclude) if exclude else None + result = [] + for dirpath, dirnames, filenames in os.walk(root): + realdir = os.path.realpath(dirpath) + if excludepath and (realdir == excludepath or realdir.startswith(excludepath + os.sep)): + dirnames[:] = [] + continue + for name in filenames: + if is_audio_file(os.path.join(dirpath, name)): + result.append(os.path.join(dirpath, name)) + result.sort() + return result + + +def list_banks(): + root = sfz_instruments_dir() + if not os.path.isdir(root): + return [] + return sorted(name for name in os.listdir(root) if os.path.isdir(os.path.join(root, name))) + + +def bank_dir(name): + return os.path.join(sfz_instruments_dir(), sanitize_bank_name(name)) + + +def sfz_filename(name): + # The name a plugin's file selector shows for this bank. + return sanitize_bank_name(name) + ".sfz" + + +def find_sfz_files(path): + # Every .sfz directly in a bank directory. A rename leaves the old one + # behind, so a build removes what it did not write. + try: + return [n for n in os.listdir(path) if n.lower().endswith(".sfz")] + except OSError: + return [] + + +def create_bank(name): + path = bank_dir(name) + if not os.path.isdir(path): + os.makedirs(path) + return path + + +def _existing_bank_dir(name): + # A bank directory that is there, resolved and checked against its root. + # sanitize_bank_name already drops a separator, so this guards the case + # where the name is a symbolic link that points out of the instruments + # directory: os.path.isdir follows a link and rmtree would then delete + # whatever it found. + root = sfz_instruments_dir() + path = safe_resolve(root, sanitize_bank_name(name)) + if path == os.path.realpath(root): + raise ValueError("invalid bank name") + if not os.path.isdir(path): + raise ValueError("no such bank: %s" % sanitize_bank_name(name)) + return path + + +def rename_bank(name, new_name): + # The .sfz file inside carries the name of the bank (see sfz_filename), so + # a rename has to rename that file too. Without it a plugin's file selector + # would go on showing the old name until the next build. + old_path = _existing_bank_dir(name) + new_clean = sanitize_bank_name(new_name) + new_path = os.path.join(sfz_instruments_dir(), new_clean) + + if os.path.realpath(new_path) == old_path: + # The same bank. A name that sanitizes to what it already is, or a + # change of nothing at all, is not an error. + return new_clean + if os.path.exists(new_path): + raise ValueError("a bank named %s is already there" % new_clean) + + os.rename(old_path, new_path) + + # The sidecar is not touched: it holds the pad layout and the absolute + # paths that samples were copied from, and neither depends on the name. + sfz_name = sfz_filename(new_clean) + for stale in find_sfz_files(new_path): + if stale == sfz_name: + continue + os.rename(os.path.join(new_path, stale), os.path.join(new_path, sfz_name)) + break + for stale in find_sfz_files(new_path): + if stale != sfz_name: + os.remove(os.path.join(new_path, stale)) + return new_clean + + +def delete_bank(name): + # The whole directory goes: the .sfz, the sidecar, and every sample that + # was uploaded to or copied into the bank. A sample that a pad points at + # outside the bank is not touched, because it was never in here. + shutil.rmtree(_existing_bank_dir(name)) + return sanitize_bank_name(name) + + +def list_bank_samples(name): + return list_audio_files(bank_dir(name)) + + +def list_device_samples(exclude_bank=None): + exclude = bank_dir(exclude_bank) if exclude_bank else None + return list_audio_files(USER_FILES_DIR, exclude=exclude) + + +def list_usb_samples(): + result = [] + if not os.path.isdir("/media"): + return result + for label in sorted(os.listdir("/media")): + root = os.path.join("/media", label) + if os.path.isdir(root): + result.extend(list_audio_files(root)) + result.sort() + return result + + +def sample_roots(): + # The places the panel lists samples from. An external sample can only come + # from one of these, because they are the only ones it shows. + roots = [USER_FILES_DIR] + if os.path.isdir("/media"): + roots.append("/media") + return [os.path.realpath(r) for r in roots] + + +def is_allowed_source(path): + # The path of an external sample arrives from the browser, so it is not + # checked by anything before this. Without the test below a bank could copy + # any readable file on the device whose name ends in an audio extension. + for root in sample_roots(): + if path == root or path.startswith(root + os.sep): + return True + return False + + +def safe_resolve(root, name): + root = os.path.realpath(root) + path = os.path.realpath(os.path.join(root, name)) + if path != root and not path.startswith(root + os.sep): + raise ValueError("path escapes its root") + return path + + +def fmt_db(value): + if value is None: + return None + value = round(float(value), 2) + if value == int(value): + return str(int(value)) + return str(value) + + +def generate_sfz(regions): + # regions: list of dicts with key, sample, volume, pitch_keycenter, loop_mode + # + # sample= is written last on each line. The value of an SFZ opcode runs to + # the next opcode or to the end of the line, so a name that holds a space is + # read whole only in the last position. Every other opcode here is a number + # or a word from a fixed set. + lines = [] + for region in regions: + line = " key=%d" % int(region["key"]) + if region.get("pitch_keycenter") is not None: + line += " pitch_keycenter=%d" % int(region["pitch_keycenter"]) + volume = fmt_db(region.get("volume")) + if volume is not None: + line += " volume=%s" % volume + line += " loop_mode=%s" % region["loop_mode"] + line += " sample=%s" % region["sample"] + lines.append(line) + return "\n".join(lines) + "\n" + + +def validate_slot(slot): + if "sample" not in slot or not slot["sample"]: + raise ValueError("slot has no sample") + volume = slot.get("volume") + if volume is not None: + volume = float(volume) + if volume < -144 or volume > 48: + raise ValueError("volume out of range") + slot["volume"] = volume + pitch = slot.get("pitch_keycenter") + if pitch is not None: + pitch = int(pitch) + if pitch < 0 or pitch > 127: + raise ValueError("pitch_keycenter out of range") + slot["pitch_keycenter"] = pitch + loop_mode = slot.get("loop_mode", "one_shot") + if loop_mode not in LOOP_MODES: + raise ValueError("invalid loop_mode") + slot["loop_mode"] = loop_mode + return slot + + +def load_bank(name): + # Returns the saved pad layout so the UI can open a bank again for edit. + path = os.path.join(bank_dir(name), SIDECAR_NAME) + data = safe_json_load(path, dict) + slots = data.get("slots") + if not isinstance(slots, list): + slots = [] + base_note = data.get("base_note") + try: + base_note = int(base_note) + except (TypeError, ValueError): + base_note = 36 + if base_note < 0 or base_note > 127: + base_note = 36 + return { + "base_note": base_note, + "slots": [slot if isinstance(slot, dict) else None for slot in slots], + } + + +def build_bank(name, base_note, slots): + # The bank folder is the source of truth. + # Uploads land there directly. External files (device, USB) are copied + # there. The sidecar tracks the copies so a rebuild can remove stale ones. + name = sanitize_bank_name(name) + if base_note is None: + raise ValueError("base note is missing") + base_note = int(base_note) + if base_note < 0 or base_note > 127: + raise ValueError("base note out of range") + if not slots: + raise ValueError("no slots") + # slots is pad-aligned: a None entry is a free pad and keeps its note number. + if not any(slot for slot in slots): + raise ValueError("no slots") + # Only a pad that holds a sample needs a note. A bank of eight pads with a + # sample on the first one is a bank the panel offers at any base note, and + # refusing it because seven empty pads run past 127 was refusing to save + # work that is there. + last_filled = max(i for i, slot in enumerate(slots) if slot) + if base_note + last_filled >= MAX_NOTES: + raise ValueError("pad %d would need note %d, and the last note is %d" + % (last_filled + 1, base_note + last_filled, MAX_NOTES - 1)) + + path = create_bank(name) + sidecar_path = os.path.join(path, SIDECAR_NAME) + old_copied = safe_json_load(sidecar_path, dict).get("copied", {}) + + used = set(os.listdir(path)) + claimed = set() + new_copied = {} + regions = [] + + layout = [] + for i, slot in enumerate(slots): + if not slot: + layout.append(None) + continue + key = base_note + i + validate_slot(slot) + source = slot.get("source") + if source: + src = os.path.realpath(source) + if not is_allowed_source(src): + raise ValueError("sample is not in a place the panel reads: %s" % source) + if not is_audio_file(src): + raise ValueError("source is not an audio file: %s" % src) + dest = old_copied.get(src) + if not dest or dest in claimed or not is_audio_file(os.path.join(path, dest)): + dest = unique_filename(sanitize_filename(os.path.basename(src)), used) + shutil.copy2(src, os.path.join(path, dest)) + new_copied[src] = dest + claimed.add(dest) + used.add(dest) + else: + # The name is already the name of a file in the bank, so it is used + # as it is. Running it through sanitize_filename again renamed the + # file that the panel had just listed -- "My Kick.wav" was looked up + # as "My_Kick.wav" -- and no sample that came from anywhere but this + # panel's own upload could be saved. + dest = slot["sample"] + if dest != os.path.basename(dest): + raise ValueError("sample name holds a path: %s" % dest) + resolved = safe_resolve(path, dest) + if dest in claimed: + raise ValueError("sample name conflict: %s" % dest) + if not is_audio_file(resolved): + raise ValueError("sample not found in bank: %s" % dest) + + regions.append({ + "key": key, + "sample": dest, + "volume": slot.get("volume"), + "pitch_keycenter": slot.get("pitch_keycenter"), + "loop_mode": slot["loop_mode"], + }) + # The layout keeps the bank-local name, so a reload needs no source path. + layout.append({ + "sample": dest, + "volume": slot.get("volume"), + "pitch_keycenter": slot.get("pitch_keycenter"), + "loop_mode": slot["loop_mode"], + }) + + # Remove stale copies. Untracked uploads stay untouched. + for src, dest in old_copied.items(): + if src not in new_copied: + stale = os.path.join(path, dest) + if is_audio_file(stale): + os.remove(stale) + used.discard(dest) + + sfz_name = sfz_filename(name) + sfz_path = os.path.join(path, sfz_name) + with TextFileFlusher(sfz_path) as fh: + fh.write(generate_sfz(regions)) + + # An .sfz under an earlier name would else stay in the file selector of a + # plugin next to the one this build wrote. + for stale in find_sfz_files(path): + if stale != sfz_name: + os.remove(os.path.join(path, stale)) + + with TextFileFlusher(sidecar_path) as fh: + json.dump({ + "copied": new_copied, + "base_note": base_note, + "slots": layout, + }, fh, indent=2) + + return { + "ok": True, + "name": name, + "path": sfz_path, + "file": sfz_name, + "count": len(regions), + } diff --git a/mod/webserver.py b/mod/webserver.py index 3995cd41..09a47f6c 100644 --- a/mod/webserver.py +++ b/mod/webserver.py @@ -50,6 +50,12 @@ get_hardware_descriptor, get_unique_name, symbolify, ) from mod.bank import list_banks, save_banks, remove_pedalboard_from_banks +from mod.sfzbuilder import ( + AUDIO_EXTENSIONS, SIDECAR_NAME, + sanitize_filename, unique_filename, list_banks as sfz_list_banks, + list_bank_samples, list_device_samples, list_usb_samples, create_bank, build_bank, + load_bank, rename_bank, delete_bank, +) from mod.session import SESSION from modtools.utils import ( kPedalboardInfoUserOnly, kPedalboardInfoFactoryOnly, kPedalboardInfoBoth, @@ -273,6 +279,29 @@ def get_cache_time(self, path, modified, mime_type): def get_modified_time(self): return None +class IslandStaticFileHandler(web.StaticFileHandler): + """ + Serves html/js/app/, the directory of the islands. + + index.html gives each of its own files a "?v=" query and the server gives + every static file a cache of one year. An island is different: it loads its + files with import() and with a that it makes itself, so those files + carry no query. With a cache of one year the browser keeps an old file -- + or keeps a 404 from a build before the file was there -- and the panel is + then empty and gives no other sign. + + This handler asks the browser to check each time. The server answers 304 if + the file did not change, so the cost is one small request for each file. + """ + def compute_etag(self): + # The default ETag of Tornado. The browser sends it back and the server + # answers 304 when the file is the same. + return web.StaticFileHandler.compute_etag(self) + + def set_extra_headers(self, path): + # set_extra_headers runs after set_headers, so this value stays. + self.set_header("Cache-Control", "no-cache") + class JsonRequestHandler(TimelessRequestHandler): def write(self, data): # FIXME: something is sending strings out, need to investigate what later.. @@ -2300,14 +2329,185 @@ def post(self): self.write(True) +def _audio_file_size(path): + # A file can go away between the walk and this call. The panel then shows + # no size, which is not an error. + try: + return os.path.getsize(path) + except OSError: + return None + +def _audio_file_entries(files): + return [ + {'fullname': f, 'basename': os.path.basename(f), 'size': _audio_file_size(f)} + for f in files + ] + +class SfzBuilderBanks(JsonRequestHandler): + def get(self): + banks = sfz_list_banks() + # The bank list shows how many samples each bank holds. The count comes + # from the same walk that the sample list uses. + counts = {} + for name in banks: + try: + counts[name] = len(list_bank_samples(name)) + except (ValueError, OSError): + pass + self.write({'ok': True, 'banks': banks, 'counts': counts}) + +class SfzBuilderBank(JsonRequestHandler): + def get(self): + try: + state = load_bank(self.get_argument('name', '')) + except ValueError as e: + self.write({'ok': False, 'error': str(e)}) + return + state['ok'] = True + self.write(state) + + @jsoncall + def post(self): + try: + path = create_bank(self.request.body.get('name')) + except ValueError as e: + self.write({'ok': False, 'error': str(e)}) + return + self.write({'ok': True, 'name': os.path.basename(path)}) + +class SfzBuilderBankRename(JsonRequestHandler): + @jsoncall + def post(self): + body = self.request.body + try: + name = rename_bank(body.get('name'), body.get('new_name')) + except (ValueError, OSError) as e: + self.write({'ok': False, 'error': str(e)}) + return + self.write({'ok': True, 'name': name}) + +class SfzBuilderBankDelete(JsonRequestHandler): + @jsoncall + def post(self): + try: + name = delete_bank(self.request.body.get('name')) + except (ValueError, OSError) as e: + self.write({'ok': False, 'error': str(e)}) + return + self.write({'ok': True, 'name': name}) + +class SfzBuilderSamples(JsonRequestHandler): + def get(self): + try: + files = list_bank_samples(self.get_argument('bank', '')) + except ValueError as e: + self.write({'ok': False, 'error': str(e)}) + return + self.write({'ok': True, 'files': _audio_file_entries(files)}) + +class SfzBuilderDevice(JsonRequestHandler): + def get(self): + try: + files = list_device_samples(self.get_argument('exclude_bank', None)) + except ValueError as e: + self.write({'ok': False, 'error': str(e)}) + return + self.write({'ok': True, 'files': _audio_file_entries(files)}) + +class SfzBuilderUsb(JsonRequestHandler): + def get(self): + self.write({'ok': True, 'files': _audio_file_entries(list_usb_samples())}) + +# The most one upload may carry, for each file and for the batch together. +# +# The number is under Tornado's own max_buffer_size, which is 100 MB and which +# this application does not change. That limit is the real ceiling: Tornado +# holds the whole request body in memory and drops the connection when the body +# goes past it, which reaches the panel as a failed request and no reason. The +# cap below is hit first, so the panel can say what happened instead. +MAX_UPLOAD_BYTES = 64 * 1024 * 1024 + +class SfzBuilderUpload(JsonRequestHandler): + def post(self): + try: + path = create_bank(self.get_argument('bank', '')) + except ValueError as e: + self.write({'ok': False, 'error': str(e)}) + return + + # Every file is checked before any file is written. The first version + # wrote as it went and returned on the first name that was not audio, + # which left the files before it in the bank and told the panel the + # upload had failed. A batch now lands whole or not at all. + entries = [] + total = 0 + for upload in self.request.files.values(): + for entry in upload: + filename = sanitize_filename(entry['filename']) + if not filename.lower().endswith(AUDIO_EXTENSIONS): + self.write({'ok': False, 'error': 'not an audio file: %s' % entry['filename']}) + return + if len(entry['body']) > MAX_UPLOAD_BYTES: + self.write({'ok': False, 'error': '%s is larger than %d MB' + % (entry['filename'], MAX_UPLOAD_BYTES // (1024 * 1024))}) + return + total += len(entry['body']) + if total > MAX_UPLOAD_BYTES: + self.write({'ok': False, 'error': 'that upload is larger than %d MB' + % (MAX_UPLOAD_BYTES // (1024 * 1024))}) + return + entries.append((filename, entry['body'])) + + used = set(os.listdir(path)) + saved = [] + for filename, body in entries: + filename = unique_filename(filename, used) + with open(os.path.join(path, filename), 'wb') as fh: + fh.write(body) + used.add(filename) + saved.append(filename) + + self.write({'ok': True, 'files': saved}) + +class SfzBuilderBuild(JsonRequestHandler): + @jsoncall + def post(self): + body = self.request.body + try: + result = build_bank(body.get('name'), body.get('base_note', 36), body.get('slots', [])) + except ValueError as e: + self.write({'ok': False, 'error': str(e)}) + return + self.write(result) + +class SfzBuilderAudio(web.StaticFileHandler): + def initialize(self): + web.StaticFileHandler.initialize(self, path='/') + + @classmethod + def get_absolute_path(cls, root, path): + return path + + # Tornado calls this with (root, absolute_path). The root is unused here, + # because get_absolute_path() above passes the full path straight through. + def validate_absolute_path(self, root, absolute_path): + absolute_path = os.path.realpath(absolute_path) + + allowed = (os.path.realpath(USER_FILES_DIR), os.path.realpath('/media')) + if not any(absolute_path.startswith(base + os.sep) for base in allowed): + raise web.HTTPError(403, 'Access denied') + + # This endpoint previews samples. It must not serve any other file. + if not absolute_path.lower().endswith(AUDIO_EXTENSIONS): + raise web.HTTPError(403, 'Not an audio file') + + if not os.path.isfile(absolute_path): + raise web.HTTPError(404) + + return absolute_path + class FilesList(JsonRequestHandler): - complete_audiofile_exts = ( - # through libsndfile - ".aif", ".aifc", ".aiff", ".au", ".bwf", ".flac", ".htk", ".iff", ".mat4", ".mat5", ".oga", ".ogg", ".opus", - ".paf", ".pvf", ".pvf5", ".sd2", ".sf", ".snd", ".svx", ".vcc", ".w64", ".wav", ".xi", - # extra through ffmpeg - ".3g2", ".3gp", ".aac", ".ac3", ".amr", ".ape", ".mp2", ".mp3", ".mpc", ".wma", - ) + complete_audiofile_exts = AUDIO_EXTENSIONS hq_audiofile_exts = ( ".aif", ".aifc", ".aiff", ".flac", ".w64", ".wav", @@ -2492,6 +2692,18 @@ def get(self): # file listing etc (r"/files/list/?", FilesList), + # sfz sound-bank builder + (r"/sfzbuilder/banks/?", SfzBuilderBanks), + (r"/sfzbuilder/bank/?", SfzBuilderBank), + (r"/sfzbuilder/bank/rename/?", SfzBuilderBankRename), + (r"/sfzbuilder/bank/delete/?", SfzBuilderBankDelete), + (r"/sfzbuilder/samples/?", SfzBuilderSamples), + (r"/sfzbuilder/device/?", SfzBuilderDevice), + (r"/sfzbuilder/usb/?", SfzBuilderUsb), + (r"/sfzbuilder/upload/?", SfzBuilderUpload), + (r"/sfzbuilder/audio/(.*)", SfzBuilderAudio), + (r"/sfzbuilder/build/?", SfzBuilderBuild), + (r"/reset/?", DashboardClean), (r"/sdk/install/?", SDKEffectInstaller), @@ -2521,6 +2733,9 @@ def get(self): (r"/([a-z]+\.html)$", TemplateHandler), (r"/(allguis|sdk|settings)$", TemplateHandler), (r"/load_template/([a-z_]+\.html)$", TemplateLoader), + # This route must come before the catch-all below. The islands + # need a different cache policy. See IslandStaticFileHandler. + (r"/js/app/(.*)", IslandStaticFileHandler, {"path": os.path.join(HTML_DIR, "js", "app")}), (r"/js/templates.js$", BulkTemplateLoader), (r"/websocket/?$", ServerWebSocket), diff --git a/setup.py b/setup.py index 8b6dd3e3..96b30cf6 100644 --- a/setup.py +++ b/setup.py @@ -111,6 +111,11 @@ def run(self): (('share/mod/html/img/social'), glob('html/img/social/*.png')), (('share/mod/html/include'), glob('html/include/*.html')), (('share/mod/html/js'), glob('html/js/*.js')), + # pi-Stomp: the SFZ builder island. Each island keeps its + # code and its stylesheet in one directory, so a new island + # needs two lines here. The globs below are not recursive. + (('share/mod/html/js/app/sfzbuilder'), glob('html/js/app/sfzbuilder/*.js')), + (('share/mod/html/js/app/sfzbuilder'), glob('html/js/app/sfzbuilder/*.css')), (('share/mod/html/js/utils'), glob('html/js/utils/*.js')), (('share/mod/html/js/lib'), glob('html/js/lib/*.js')), (('share/mod/html/js/lib/slick'), glob('html/js/lib/slick/*.js')), diff --git a/test/browser/run.sh b/test/browser/run.sh new file mode 100755 index 00000000..1199865b --- /dev/null +++ b/test/browser/run.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Runs the browser smoke test of the SFZ builder island. +# +# serve.py gives /_smoke.html from test/browser/ and every other path from +# html/. Nothing is written into html/, because html/ is what deploy.sh sends +# to the device: an earlier version copied the page there and raced with rsync. +# +# The test needs headless Chrome. Without Chrome the script says so and stops +# with 0, so `make test` still passes on a machine that has no browser. +set -u + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +PORT="${SMOKE_PORT:-8899}" + +CHROME="" +for candidate in \ + "${CHROME_BIN:-}" \ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ + "/Applications/Chromium.app/Contents/MacOS/Chromium" \ + "$(command -v google-chrome-stable 2>/dev/null)" \ + "$(command -v google-chrome 2>/dev/null)" \ + "$(command -v chromium 2>/dev/null)" \ + "$(command -v chromium-browser 2>/dev/null)"; do + if [ -n "$candidate" ] && [ -x "$candidate" ]; then CHROME="$candidate"; break; fi +done + +if [ -z "$CHROME" ]; then + echo "smoke: no headless Chrome on this machine, so the test did not run." + echo "smoke: set CHROME_BIN to run it." + exit 0 +fi + +cleanup() { + [ -n "${SERVER_PID:-}" ] && kill "$SERVER_PID" 2>/dev/null + return 0 +} +trap cleanup EXIT + +python3 "$ROOT/test/browser/serve.py" "$PORT" & +SERVER_PID=$! + +# Wait for the server to answer. +for _ in $(seq 1 40); do + if curl -fsS -o /dev/null "http://127.0.0.1:$PORT/_smoke.html" 2>/dev/null; then break; fi + sleep 0.1 +done + +OUT="$("$CHROME" --headless --disable-gpu --no-sandbox \ + --virtual-time-budget=12000 \ + --dump-dom "http://127.0.0.1:$PORT/_smoke.html" 2>/dev/null \ + | python3 -c " +import sys, html, re +d = sys.stdin.read() +m = re.search(r'SMOKE_START(.*?)SMOKE_END', d, re.S) +print(html.unescape(m.group(1)).strip() if m else 'FAIL the page did not run to the end') +")" + +echo "$OUT" +if echo "$OUT" | grep -q '^FAIL'; then + echo + echo "smoke: FAILED" + exit 1 +fi +echo +echo "smoke: all checks passed" diff --git a/test/browser/serve.py b/test/browser/serve.py new file mode 100755 index 00000000..ff023d9f --- /dev/null +++ b/test/browser/serve.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +""" +Serves html/ for the browser smoke test. + +The test page must come from the same place that gives the browser the rest of +the files, because the island reads its own files with a relative path. An +earlier version of this test copied the page into html/. That was wrong: html/ +is the directory that deploy.sh sends to the device, and the copy and the +deletion of the page raced with rsync. + +This server writes nothing. It gives one path, /_smoke.html, from +test/browser/smoke.html, and every other path from html/. +""" + +import functools +import http.server +import os +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +HTML_DIR = os.path.join(ROOT, "html") +SMOKE_PAGE = os.path.join(ROOT, "test", "browser", "smoke.html") + + +class Handler(http.server.SimpleHTTPRequestHandler): + def translate_path(self, path): + if path.split("?")[0].split("#")[0] == "/_smoke.html": + return SMOKE_PAGE + return super().translate_path(path) + + def log_message(self, fmt, *args): + pass + + +def main(): + port = int(sys.argv[1]) if len(sys.argv) > 1 else 8899 + handler = functools.partial(Handler, directory=HTML_DIR) + with http.server.ThreadingHTTPServer(("127.0.0.1", port), handler) as httpd: + httpd.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/test/browser/smoke.html b/test/browser/smoke.html new file mode 100644 index 00000000..93ae14ec --- /dev/null +++ b/test/browser/smoke.html @@ -0,0 +1,588 @@ + + + + +SFZ builder browser smoke test + + + + + + + +
+
+
+

+
+
+
+
+
+
diff --git a/test/js/model.test.js b/test/js/model.test.js
new file mode 100644
index 00000000..3fbc1028
--- /dev/null
+++ b/test/js/model.test.js
@@ -0,0 +1,178 @@
+// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import * as model from '../../html/js/app/sfzbuilder/model.js'
+
+test('noteName gives the name of a MIDI note', () => {
+    assert.equal(model.noteName(0), 'C-1')
+    assert.equal(model.noteName(36), 'C2')
+    assert.equal(model.noteName(37), 'C#2')
+    assert.equal(model.noteName(60), 'C4')
+    assert.equal(model.noteName(127), 'G9')
+})
+
+test('clampBaseNote keeps the value between 0 and 127', () => {
+    assert.equal(model.clampBaseNote('36'), 36)
+    assert.equal(model.clampBaseNote(-5), 0)
+    assert.equal(model.clampBaseNote(200), 127)
+})
+
+test('clampBaseNote gives the default value for bad input', () => {
+    assert.equal(model.clampBaseNote(''), 36)
+    assert.equal(model.clampBaseNote('abc'), 36)
+    assert.equal(model.clampBaseNote(null), 36)
+})
+
+test('clampPadCount keeps the value between 1 and 128', () => {
+    assert.equal(model.clampPadCount('8'), 8)
+    assert.equal(model.clampPadCount(0), 1)
+    assert.equal(model.clampPadCount(999), 128)
+    assert.equal(model.clampPadCount('abc'), 1)
+})
+
+test('resizePads keeps each pad at its index', () => {
+    const kick = { sample: 'kick.wav', source: null, volume: 0, pitch: null, loop: 'one_shot' }
+    const slots = [null, kick, null]
+    assert.deepEqual(model.resizePads(slots, 2), [null, kick])
+})
+
+test('resizePads adds empty pads at the end', () => {
+    assert.deepEqual(model.resizePads([], 3), [null, null, null])
+})
+
+test('resizePads does not change the input array', () => {
+    const slots = [null, null]
+    model.resizePads(slots, 5)
+    assert.equal(slots.length, 2)
+})
+
+test('noteToken gives the number and the name', () => {
+    assert.deepEqual(model.noteToken(36), { note: 36, name: 'C2', inRange: true })
+})
+
+test('noteToken marks a note above 127 as out of range', () => {
+    assert.deepEqual(model.noteToken(128), { note: 128, name: '--', inRange: false })
+})
+
+test('clampCols keeps the count from 2 to 8', () => {
+    assert.equal(model.clampCols('4'), 4)
+    assert.equal(model.clampCols(1), 2)
+    assert.equal(model.clampCols(8), 8)
+    assert.equal(model.clampCols(99), 8)
+    assert.equal(model.clampCols(null), model.DEFAULT_COLS)
+    assert.equal(model.clampCols('abc'), model.DEFAULT_COLS)
+})
+
+test('formatSize gives a short size', () => {
+    assert.equal(model.formatSize(0), '0 B')
+    assert.equal(model.formatSize(512), '512 B')
+    assert.equal(model.formatSize(412000), '402 KB')
+    assert.equal(model.formatSize(1258291), '1.2 MB')
+})
+
+test('formatSize gives nothing for an unknown size', () => {
+    assert.equal(model.formatSize(null), '')
+    assert.equal(model.formatSize(undefined), '')
+    assert.equal(model.formatSize(-1), '')
+})
+
+test('isAudioName accepts the types that the server accepts', () => {
+    assert.equal(model.isAudioName('Kick_01.wav'), true)
+    assert.equal(model.isAudioName('LOUD.WAV'), true)
+    assert.equal(model.isAudioName('pad.flac'), true)
+    assert.equal(model.isAudioName('take.aiff'), true)
+    assert.equal(model.isAudioName('notes.txt'), false)
+    assert.equal(model.isAudioName('cover.png'), false)
+    assert.equal(model.isAudioName('wav'), false)
+    assert.equal(model.isAudioName(''), false)
+})
+
+// The server refuses the full batch if one file is not audio, and it refuses it
+// after it writes the files before that one. Thus the panel divides the files
+// first. A drop from the desktop has no filter.
+test('splitAudioFiles divides a drop into audio and the rest', () => {
+    const files = [
+        { name: 'Kick.wav' },
+        { name: '.DS_Store' },
+        { name: 'Snare.FLAC' },
+        { name: 'cover.jpg' },
+    ]
+    const split = model.splitAudioFiles(files)
+    assert.deepEqual(split.accepted.map((f) => f.name), ['Kick.wav', 'Snare.FLAC'])
+    assert.deepEqual(split.rejected.map((f) => f.name), ['.DS_Store', 'cover.jpg'])
+})
+
+test('splitAudioFiles gives two empty lists for an empty drop', () => {
+    const split = model.splitAudioFiles([])
+    assert.deepEqual(split.accepted, [])
+    assert.deepEqual(split.rejected, [])
+})
+
+test('alignSavedSlots keeps the empty pads', () => {
+    const saved = [null, { sample: 'snare.wav', volume: -3, pitch_keycenter: 40, loop_mode: 'no_loop' }]
+    const aligned = model.alignSavedSlots(saved)
+    assert.equal(aligned[0], null)
+    assert.deepEqual(aligned[1], {
+        sample: 'snare.wav', source: null, volume: -3, pitch: 40, loop: 'no_loop',
+    })
+})
+
+test('alignSavedSlots gives the default values for missing fields', () => {
+    const aligned = model.alignSavedSlots([{ sample: 'hat.wav', volume: 0 }])
+    assert.equal(aligned[0].pitch, null)
+    assert.equal(aligned[0].loop, 'one_shot')
+})
+
+test('buildPayload sends null for an empty pad', () => {
+    const slots = [
+        null,
+        { sample: 'kick.wav', source: null, volume: 0, pitch: null, loop: 'one_shot' },
+    ]
+    const payload = model.buildPayload(slots)
+    assert.equal(payload.filled, 1)
+    assert.equal(payload.aligned[0], null)
+    assert.deepEqual(payload.aligned[1], {
+        sample: 'kick.wav', source: null, volume: 0, pitch_keycenter: null, loop_mode: 'one_shot',
+    })
+})
+
+test('buildPayload counts no full pads in an empty bank', () => {
+    assert.equal(model.buildPayload([null, null]).filled, 0)
+})
+
+test('filterFiles ignores the case of the letters', () => {
+    const files = [{ basename: 'Kick.wav', fullname: '/a/Kick.wav' }, { basename: 'snare.wav', fullname: '/a/snare.wav' }]
+    assert.deepEqual(model.filterFiles(files, 'KICK'), [files[0]])
+})
+
+test('filterFiles gives every file for an empty term', () => {
+    const files = [{ basename: 'a.wav', fullname: '/a.wav' }]
+    assert.equal(model.filterFiles(files, '').length, 1)
+})
+
+test('slotFromSample keeps the path for a file outside the bank', () => {
+    const file = { basename: 'kick.wav', fullname: '/media/usb/kick.wav' }
+    assert.equal(model.slotFromSample(file, 'usb').source, '/media/usb/kick.wav')
+    assert.equal(model.slotFromSample(file, 'bank').source, null)
+})
+
+test('pickInitialBank opens the bank of the last visit', () => {
+    assert.equal(model.pickInitialBank(['Drums', 'Keys'], 'Keys'), 'Keys')
+})
+
+test('pickInitialBank gives the first bank when the last one is gone', () => {
+    assert.equal(model.pickInitialBank(['Drums', 'Keys'], 'Deleted'), 'Drums')
+})
+
+test('pickInitialBank gives the first bank when nothing is remembered', () => {
+    assert.equal(model.pickInitialBank(['Drums', 'Keys'], null), 'Drums')
+    assert.equal(model.pickInitialBank(['Drums'], ''), 'Drums')
+    assert.equal(model.pickInitialBank(['Drums'], undefined), 'Drums')
+})
+
+test('pickInitialBank gives no bank when the device has none', () => {
+    assert.equal(model.pickInitialBank([], 'Keys'), '')
+    assert.equal(model.pickInitialBank([], null), '')
+})
diff --git a/test/js/package.json b/test/js/package.json
new file mode 100644
index 00000000..3dbc1ca5
--- /dev/null
+++ b/test/js/package.json
@@ -0,0 +1,3 @@
+{
+  "type": "module"
+}
diff --git a/test/test_setup_data_files.py b/test/test_setup_data_files.py
new file mode 100644
index 00000000..f769c66c
--- /dev/null
+++ b/test/test_setup_data_files.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+"""
+Guards the install list of setup.py.
+
+setup.py names each directory of html/ with a glob that is not recursive. Thus a
+new directory is absent from the package and the browser reads nothing. The
+panel of that feature is then empty and gives no other sign.
+
+An island keeps its code and its stylesheet in html/js/app//. This test
+reads the globs of setup.py and asks that each of those files is in one of them.
+"""
+
+import ast
+import glob as globmodule
+import os
+import unittest
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ISLAND_ROOT = os.path.join(ROOT, "html", "js", "app")
+
+
+def installed_files():
+    """Gives every file that the data_files list of setup.py installs.
+
+    The test runs the same glob() calls that setup.py runs. fnmatch is not
+    correct here, because its * goes past a / and thus 'html/js/*.js' would
+    appear to hold a file in a directory below.
+    """
+    with open(os.path.join(ROOT, "setup.py")) as fh:
+        tree = ast.parse(fh.read())
+
+    patterns = []
+    for node in ast.walk(tree):
+        # Each entry looks like glob('html/js/app/sfzbuilder/*.js').
+        if not isinstance(node, ast.Call):
+            continue
+        if not isinstance(node.func, ast.Name) or node.func.id != "glob":
+            continue
+        if len(node.args) != 1 or not isinstance(node.args[0], ast.Constant):
+            continue
+        patterns.append(node.args[0].value)
+
+    found = set()
+    for pattern in patterns:
+        for path in globmodule.glob(os.path.join(ROOT, pattern)):
+            found.add(os.path.relpath(path, ROOT))
+    return patterns, found
+
+
+class TestSetupDataFiles(unittest.TestCase):
+    def test_island_files_are_installed(self):
+        patterns, installed = installed_files()
+        self.assertTrue(patterns, "setup.py holds no glob() call")
+
+        missing = []
+        for dirpath, dirnames, filenames in os.walk(ISLAND_ROOT):
+            dirnames[:] = [d for d in dirnames if d != "node_modules"]
+            for name in filenames:
+                if not name.endswith((".js", ".css")):
+                    continue
+                path = os.path.relpath(os.path.join(dirpath, name), ROOT)
+                if path not in installed:
+                    missing.append(path)
+
+        self.assertEqual(
+            missing, [],
+            "these island files are not in the data_files list of setup.py, so "
+            "the device does not get them:\n  " + "\n  ".join(missing))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/test/test_sfzbuilder.py b/test/test_sfzbuilder.py
new file mode 100755
index 00000000..620da2c9
--- /dev/null
+++ b/test/test_sfzbuilder.py
@@ -0,0 +1,510 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+import os
+import shutil
+import tempfile
+import unittest
+
+from mod import sfzbuilder
+
+
+def make_wav(path):
+    os.makedirs(os.path.dirname(path), exist_ok=True)
+    with open(path, 'wb') as fh:
+        fh.write(b'RIFF----WAVEfmt ')
+
+
+class TestSanitize(unittest.TestCase):
+    def test_spaces_are_kept(self):
+        # generate_sfz writes sample= last, where the value runs to the end of
+        # the line, so a space in a name is safe.
+        self.assertEqual(sfzbuilder.sanitize_filename("kick drum.wav"), "kick drum.wav")
+
+    def test_surrounding_spaces_are_dropped(self):
+        self.assertEqual(sfzbuilder.sanitize_filename("  kick  .wav"), "kick.wav")
+
+    def test_sfz_breaking_chars_removed(self):
+        self.assertEqual(sfzbuilder.sanitize_filename("kick=1<2#3.wav"), "kick_1_2_3.wav")
+
+    def test_basename_only(self):
+        self.assertEqual(sfzbuilder.sanitize_filename("/a/b/x.wav"), "x.wav")
+
+    def test_empty_stem(self):
+        self.assertEqual(sfzbuilder.sanitize_filename(".wav"), "sample.wav")
+
+    def test_case_kept(self):
+        self.assertEqual(sfzbuilder.sanitize_filename("Snare.WAV"), "Snare.WAV")
+
+    def test_none_input(self):
+        self.assertEqual(sfzbuilder.sanitize_filename(None), "sample")
+
+    def test_unique_filename(self):
+        used = {"a.wav"}
+        self.assertEqual(sfzbuilder.unique_filename("a.wav", used), "a_1.wav")
+        used.add("a_1.wav")
+        self.assertEqual(sfzbuilder.unique_filename("a.wav", used), "a_2.wav")
+
+    def test_bank_name(self):
+        self.assertEqual(sfzbuilder.sanitize_bank_name("My Bank"), "My_Bank")
+        with self.assertRaises(ValueError):
+            sfzbuilder.sanitize_bank_name("")
+        with self.assertRaises(ValueError):
+            sfzbuilder.sanitize_bank_name("..")
+
+
+class TestGenerateSfz(unittest.TestCase):
+    def test_golden_output(self):
+        regions = [
+            {"key": 36, "sample": "01_kick.wav", "volume": -3, "pitch_keycenter": None, "loop_mode": "one_shot"},
+            {"key": 37, "sample": "02_snare.wav", "volume": None, "pitch_keycenter": 60, "loop_mode": "no_loop"},
+        ]
+        expected = (
+            " key=36 volume=-3 loop_mode=one_shot sample=01_kick.wav\n"
+            " key=37 pitch_keycenter=60 loop_mode=no_loop sample=02_snare.wav\n"
+        )
+        self.assertEqual(sfzbuilder.generate_sfz(regions), expected)
+
+    def test_volume_formatting(self):
+        self.assertEqual(sfzbuilder.fmt_db(-3), "-3")
+        self.assertEqual(sfzbuilder.fmt_db(2.5), "2.5")
+        self.assertIsNone(sfzbuilder.fmt_db(None))
+
+
+class TestBuildBank(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.orig_user_files_dir = sfzbuilder.USER_FILES_DIR
+        sfzbuilder.USER_FILES_DIR = self.tmp
+
+    def tearDown(self):
+        sfzbuilder.USER_FILES_DIR = self.orig_user_files_dir
+        shutil.rmtree(self.tmp, ignore_errors=True)
+
+    def test_build_with_upload_and_external(self):
+        # Simulate an upload: file already in the bank folder.
+        make_wav(os.path.join(self.tmp, "SFZ Instruments", "Kit", "kick.wav"))
+        # Device source outside the bank folder.
+        external = os.path.join(self.tmp, "Audio Samples", "loop.wav")
+        make_wav(external)
+
+        slots = [
+            {"sample": "kick.wav", "source": None, "volume": -6, "loop_mode": "one_shot"},
+            {"sample": "loop.wav", "source": external, "volume": None, "loop_mode": "no_loop"},
+        ]
+        result = sfzbuilder.build_bank("Kit", 36, slots)
+
+        self.assertTrue(result["ok"])
+        self.assertEqual(result["count"], 2)
+        bank_dir = os.path.join(self.tmp, "SFZ Instruments", "Kit")
+        self.assertTrue(os.path.isfile(os.path.join(bank_dir, "Kit.sfz")))
+        self.assertTrue(os.path.isfile(os.path.join(bank_dir, "loop.wav")))
+
+        with open(os.path.join(bank_dir, "Kit.sfz")) as fh:
+            text = fh.read()
+        self.assertIn("key=36 volume=-6 loop_mode=one_shot sample=kick.wav", text)
+        self.assertIn("key=37 loop_mode=no_loop sample=loop.wav", text)
+
+        # Sidecar tracks the copy.
+        with open(os.path.join(bank_dir, "bank.json")) as fh:
+            import json
+            sidecar = json.load(fh)
+        self.assertEqual(sidecar["copied"], {os.path.realpath(external): "loop.wav"})
+
+    def test_rebuild_removes_stale_copy_keeps_upload(self):
+        make_wav(os.path.join(self.tmp, "SFZ Instruments", "Kit", "kick.wav"))
+        external = os.path.join(self.tmp, "Audio Samples", "loop.wav")
+        make_wav(external)
+        bank_dir = os.path.join(self.tmp, "SFZ Instruments", "Kit")
+
+        sfzbuilder.build_bank("Kit", 36, [
+            {"sample": "kick.wav", "source": None, "loop_mode": "one_shot"},
+            {"sample": "loop.wav", "source": external, "loop_mode": "one_shot"},
+        ])
+        self.assertTrue(os.path.isfile(os.path.join(bank_dir, "loop.wav")))
+
+        # Rebuild without the external sample.
+        sfzbuilder.build_bank("Kit", 36, [
+            {"sample": "kick.wav", "source": None, "loop_mode": "one_shot"},
+        ])
+        self.assertFalse(os.path.isfile(os.path.join(bank_dir, "loop.wav")))
+        self.assertTrue(os.path.isfile(os.path.join(bank_dir, "kick.wav")))
+
+    def test_duplicate_basenames_get_suffix(self):
+        d1 = os.path.join(self.tmp, "Audio Samples", "A")
+        d2 = os.path.join(self.tmp, "Audio Samples", "B")
+        make_wav(os.path.join(d1, "hit.wav"))
+        make_wav(os.path.join(d2, "hit.wav"))
+
+        result = sfzbuilder.build_bank("Kit", 36, [
+            {"sample": "hit.wav", "source": os.path.join(d1, "hit.wav"), "loop_mode": "one_shot"},
+            {"sample": "hit_1.wav", "source": os.path.join(d2, "hit.wav"), "loop_mode": "one_shot"},
+        ])
+        bank_dir = os.path.join(self.tmp, "SFZ Instruments", "Kit")
+        self.assertTrue(os.path.isfile(os.path.join(bank_dir, "hit.wav")))
+        self.assertTrue(os.path.isfile(os.path.join(bank_dir, "hit_1.wav")))
+        with open(os.path.join(bank_dir, "Kit.sfz")) as fh:
+            text = fh.read()
+        self.assertIn("sample=hit.wav", text)
+        self.assertIn("sample=hit_1.wav", text)
+        self.assertEqual(result["count"], 2)
+
+    def test_errors(self):
+        make_wav(os.path.join(self.tmp, "Audio Samples", "a.wav"))
+        with self.assertRaises(ValueError):
+            sfzbuilder.build_bank("Kit", 36, [])
+        with self.assertRaises(ValueError):
+            sfzbuilder.build_bank("Kit", 128, [{"sample": "a.wav", "source": os.path.join(self.tmp, "Audio Samples", "a.wav")}])
+        with self.assertRaises(ValueError):
+            sfzbuilder.build_bank("Kit", 120, [{"sample": "a.wav", "source": os.path.join(self.tmp, "Audio Samples", "a.wav")}] * 9)
+        with self.assertRaises(ValueError):
+            sfzbuilder.build_bank("Kit", 36, [{"sample": "a.wav", "source": os.path.join(self.tmp, "Audio Samples", "a.wav"), "volume": 100}])
+        with self.assertRaises(ValueError):
+            sfzbuilder.build_bank("Kit", 36, [{"sample": "missing.wav", "source": None, "loop_mode": "one_shot"}])
+
+    def test_device_list_excludes_current_bank(self):
+        make_wav(os.path.join(self.tmp, "SFZ Instruments", "Kit", "kick.wav"))
+        make_wav(os.path.join(self.tmp, "Audio Samples", "loop.wav"))
+        files = sfzbuilder.list_device_samples(exclude_bank="Kit")
+        self.assertEqual(files, [os.path.join(self.tmp, "Audio Samples", "loop.wav")])
+
+    def test_bank_roundtrip_listing(self):
+        make_wav(os.path.join(self.tmp, "SFZ Instruments", "Kit", "kick.wav"))
+        self.assertEqual(sfzbuilder.list_banks(), ["Kit"])
+        self.assertEqual(len(sfzbuilder.list_bank_samples("Kit")), 1)
+        self.assertEqual(sfzbuilder.list_usb_samples(), [])
+
+    def test_free_pad_keeps_its_note_number(self):
+        make_wav(os.path.join(self.tmp, "SFZ Instruments", "Kit", "kick.wav"))
+        make_wav(os.path.join(self.tmp, "SFZ Instruments", "Kit", "snare.wav"))
+
+        # Pad 2 is free. The sample on pad 3 must keep key 38, not key 37.
+        result = sfzbuilder.build_bank("Kit", 36, [
+            {"sample": "kick.wav", "source": None, "loop_mode": "one_shot"},
+            None,
+            {"sample": "snare.wav", "source": None, "loop_mode": "one_shot"},
+        ])
+        self.assertEqual(result["count"], 2)
+
+        with open(os.path.join(self.tmp, "SFZ Instruments", "Kit", "Kit.sfz")) as fh:
+            text = fh.read()
+        self.assertIn("key=36 loop_mode=one_shot sample=kick.wav", text)
+        self.assertIn("key=38 loop_mode=one_shot sample=snare.wav", text)
+        self.assertNotIn("key=37", text)
+
+    def test_build_rejects_only_free_pads(self):
+        sfzbuilder.create_bank("Kit")
+        with self.assertRaises(ValueError):
+            sfzbuilder.build_bank("Kit", 36, [None, None])
+
+
+class TestLoadBank(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.orig_user_files_dir = sfzbuilder.USER_FILES_DIR
+        sfzbuilder.USER_FILES_DIR = self.tmp
+
+    def tearDown(self):
+        sfzbuilder.USER_FILES_DIR = self.orig_user_files_dir
+        shutil.rmtree(self.tmp, ignore_errors=True)
+
+    def test_layout_survives_a_build(self):
+        make_wav(os.path.join(self.tmp, "SFZ Instruments", "Kit", "kick.wav"))
+        external = os.path.join(self.tmp, "Audio Samples", "loop.wav")
+        make_wav(external)
+
+        sfzbuilder.build_bank("Kit", 40, [
+            {"sample": "kick.wav", "source": None, "volume": -6, "loop_mode": "one_shot"},
+            None,
+            {"sample": "loop.wav", "source": external, "loop_mode": "no_loop"},
+        ])
+
+        state = sfzbuilder.load_bank("Kit")
+        self.assertEqual(state["base_note"], 40)
+        self.assertEqual(len(state["slots"]), 3)
+        self.assertEqual(state["slots"][0]["sample"], "kick.wav")
+        self.assertEqual(state["slots"][0]["volume"], -6)
+        self.assertIsNone(state["slots"][1])
+        # The external file is copied into the bank, so the layout is bank-local.
+        self.assertEqual(state["slots"][2]["sample"], "loop.wav")
+        self.assertEqual(state["slots"][2]["loop_mode"], "no_loop")
+
+    def test_bank_without_a_sidecar(self):
+        make_wav(os.path.join(self.tmp, "SFZ Instruments", "Kit", "kick.wav"))
+        state = sfzbuilder.load_bank("Kit")
+        self.assertEqual(state["base_note"], 36)
+        self.assertEqual(state["slots"], [])
+
+    def test_unsafe_name_stays_inside_the_root(self):
+        # sanitize_bank_name() contains a traversal instead of rejecting it.
+        # The lookup must stay under the SFZ Instruments folder.
+        state = sfzbuilder.load_bank("../escape")
+        self.assertEqual(state["slots"], [])
+        self.assertTrue(sfzbuilder.bank_dir("../escape").startswith(
+            sfzbuilder.sfz_instruments_dir() + os.sep))
+
+    def test_empty_name_is_rejected(self):
+        with self.assertRaises(ValueError):
+            sfzbuilder.load_bank("")
+
+
+if __name__ == "__main__":
+    unittest.main()
+
+
+class TestBankFilename(unittest.TestCase):
+    # The file selector of a plugin lists an SFZ file by its basename alone, so
+    # every bank writing "bank.sfz" gave a column of identical rows.
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self._real = sfzbuilder.USER_FILES_DIR
+        sfzbuilder.USER_FILES_DIR = self.tmp
+
+    def tearDown(self):
+        sfzbuilder.USER_FILES_DIR = self._real
+        shutil.rmtree(self.tmp, ignore_errors=True)
+
+    def _build(self, bank, sample="kick.wav"):
+        path = sfzbuilder.create_bank(bank)
+        make_wav(os.path.join(path, sample))
+        return sfzbuilder.build_bank(bank, 36, [
+            {"sample": sample, "source": None, "loop_mode": "one_shot"},
+        ])
+
+    def test_the_sfz_carries_the_name_of_its_bank(self):
+        self.assertEqual(self._build("Drumkit A")["file"], "Drumkit_A.sfz")
+        self.assertEqual(self._build("Foley")["file"], "Foley.sfz")
+
+    def test_two_banks_do_not_share_a_filename(self):
+        first = self._build("Drumkit A")["file"]
+        second = self._build("Foley")["file"]
+        self.assertNotEqual(first, second)
+
+    def test_a_build_removes_an_sfz_it_did_not_write(self):
+        path = sfzbuilder.create_bank("Kit")
+        with open(os.path.join(path, "OldName.sfz"), "w") as fh:
+            fh.write(" key=36 sample=gone.wav\n")
+        self._build("Kit")
+        self.assertEqual(sorted(sfzbuilder.find_sfz_files(path)), ["Kit.sfz"])
+
+
+class TestSampleNamesWithSpaces(unittest.TestCase):
+    # A file that reached the bank by any road but this panel's own upload --
+    # the file manager, scp, an unzip -- keeps its name, and could not be saved
+    # while build_bank ran that name through sanitize_filename a second time.
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self._real = sfzbuilder.USER_FILES_DIR
+        sfzbuilder.USER_FILES_DIR = self.tmp
+
+    def tearDown(self):
+        sfzbuilder.USER_FILES_DIR = self._real
+        shutil.rmtree(self.tmp, ignore_errors=True)
+
+    def test_a_sample_with_a_space_can_be_saved(self):
+        path = sfzbuilder.create_bank("Kit")
+        make_wav(os.path.join(path, "My Kick.wav"))
+        listed = [os.path.basename(f) for f in sfzbuilder.list_bank_samples("Kit")]
+        self.assertEqual(listed, ["My Kick.wav"])
+
+        result = sfzbuilder.build_bank("Kit", 36, [
+            {"sample": "My Kick.wav", "source": None, "loop_mode": "one_shot"},
+        ])
+        self.assertEqual(result["count"], 1)
+        with open(os.path.join(path, "Kit.sfz")) as fh:
+            self.assertIn("sample=My Kick.wav", fh.read())
+
+    def test_the_name_is_last_so_the_space_reads_whole(self):
+        # Everything after "sample=" to the end of the line is the name.
+        text = sfzbuilder.generate_sfz([
+            {"key": 36, "sample": "My Kick.wav", "volume": -6,
+             "pitch_keycenter": 60, "loop_mode": "one_shot"},
+        ])
+        self.assertTrue(text.rstrip("\n").endswith("sample=My Kick.wav"))
+
+    def test_a_name_that_holds_a_path_is_refused(self):
+        path = sfzbuilder.create_bank("Kit")
+        make_wav(os.path.join(path, "kick.wav"))
+        with self.assertRaises(ValueError):
+            sfzbuilder.build_bank("Kit", 36, [
+                {"sample": "../../kick.wav", "source": None, "loop_mode": "one_shot"},
+            ])
+
+
+class TestRenameAndDelete(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self._real = sfzbuilder.USER_FILES_DIR
+        sfzbuilder.USER_FILES_DIR = self.tmp
+
+    def tearDown(self):
+        sfzbuilder.USER_FILES_DIR = self._real
+        shutil.rmtree(self.tmp, ignore_errors=True)
+
+    def _build(self, bank, sample="kick.wav"):
+        path = sfzbuilder.create_bank(bank)
+        make_wav(os.path.join(path, sample))
+        sfzbuilder.build_bank(bank, 36, [
+            {"sample": sample, "source": None, "loop_mode": "one_shot"},
+        ])
+        return path
+
+    def test_rename_moves_the_directory(self):
+        self._build("Kit")
+        self.assertEqual(sfzbuilder.rename_bank("Kit", "Drumkit A"), "Drumkit_A")
+        self.assertEqual(sfzbuilder.list_banks(), ["Drumkit_A"])
+
+    def test_rename_renames_the_sfz_with_it(self):
+        # A plugin's file selector shows the basename, so an .sfz left under
+        # the old name would keep showing the old name until the next build.
+        self._build("Kit")
+        sfzbuilder.rename_bank("Kit", "Drumkit A")
+        path = sfzbuilder.bank_dir("Drumkit A")
+        self.assertEqual(sfzbuilder.find_sfz_files(path), ["Drumkit_A.sfz"])
+
+    def test_rename_keeps_the_samples_and_the_layout(self):
+        self._build("Kit")
+        sfzbuilder.rename_bank("Kit", "Foley")
+        state = sfzbuilder.load_bank("Foley")
+        self.assertEqual(state["base_note"], 36)
+        self.assertEqual(state["slots"][0]["sample"], "kick.wav")
+        listed = [os.path.basename(f) for f in sfzbuilder.list_bank_samples("Foley")]
+        self.assertEqual(listed, ["kick.wav"])
+
+    def test_rename_onto_an_existing_bank_is_refused(self):
+        self._build("Kit")
+        self._build("Foley")
+        with self.assertRaises(ValueError):
+            sfzbuilder.rename_bank("Kit", "Foley")
+        self.assertEqual(sfzbuilder.list_banks(), ["Foley", "Kit"])
+
+    def test_rename_to_the_same_name_is_not_an_error(self):
+        # "Kit A" and "Kit_A" sanitize to the same directory, so this is the
+        # path a rename that only changes a space takes.
+        self._build("Kit_A")
+        self.assertEqual(sfzbuilder.rename_bank("Kit_A", "Kit A"), "Kit_A")
+        self.assertEqual(sfzbuilder.list_banks(), ["Kit_A"])
+
+    def test_rename_of_a_bank_that_is_not_there(self):
+        with self.assertRaises(ValueError):
+            sfzbuilder.rename_bank("Gone", "Kit")
+
+    def test_rename_to_an_empty_name_is_refused(self):
+        self._build("Kit")
+        with self.assertRaises(ValueError):
+            sfzbuilder.rename_bank("Kit", "...")
+        self.assertEqual(sfzbuilder.list_banks(), ["Kit"])
+
+    def test_delete_removes_the_bank(self):
+        self._build("Kit")
+        self._build("Foley")
+        self.assertEqual(sfzbuilder.delete_bank("Kit"), "Kit")
+        self.assertEqual(sfzbuilder.list_banks(), ["Foley"])
+
+    def test_delete_of_a_bank_that_is_not_there(self):
+        with self.assertRaises(ValueError):
+            sfzbuilder.delete_bank("Gone")
+
+    def test_delete_does_not_follow_a_link_out_of_the_root(self):
+        # rmtree on a name that resolves outside the instruments directory
+        # would take the target with it.
+        outside = os.path.join(self.tmp, "keep")
+        make_wav(os.path.join(outside, "precious.wav"))
+        root = sfzbuilder.sfz_instruments_dir()
+        os.makedirs(root, exist_ok=True)
+        os.symlink(outside, os.path.join(root, "Escape"))
+        with self.assertRaises(ValueError):
+            sfzbuilder.delete_bank("Escape")
+        self.assertTrue(os.path.isfile(os.path.join(outside, "precious.wav")))
+
+
+class TestNoteCeiling(unittest.TestCase):
+    # A free pad needs no note, so free pads at the end must not stop a save.
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self._real = sfzbuilder.USER_FILES_DIR
+        sfzbuilder.USER_FILES_DIR = self.tmp
+        path = sfzbuilder.create_bank("Kit")
+        make_wav(os.path.join(path, "kick.wav"))
+
+    def tearDown(self):
+        sfzbuilder.USER_FILES_DIR = self._real
+        shutil.rmtree(self.tmp, ignore_errors=True)
+
+    def _slot(self):
+        return {"sample": "kick.wav", "source": None, "loop_mode": "one_shot"}
+
+    def test_free_pads_past_the_last_note_are_allowed(self):
+        # Base 120 with 8 pads runs to note 127 on the last pad. Only the first
+        # holds a sample, so the other seven need nothing.
+        slots = [self._slot()] + [None] * 7
+        self.assertEqual(sfzbuilder.build_bank("Kit", 126, slots)["count"], 1)
+
+    def test_a_filled_pad_past_the_last_note_is_refused(self):
+        slots = [None] * 7 + [self._slot()]
+        with self.assertRaises(ValueError):
+            sfzbuilder.build_bank("Kit", 126, slots)
+
+    def test_the_last_note_itself_is_allowed(self):
+        slots = [None, self._slot()]
+        self.assertEqual(sfzbuilder.build_bank("Kit", 126, slots)["count"], 1)
+
+
+class TestExternalSource(unittest.TestCase):
+    # The path of an external sample comes from the browser. Nothing else checks
+    # it, so build_bank must, or a bank could copy any readable audio file.
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.files = os.path.join(self.tmp, "user-files")
+        os.makedirs(self.files)
+        self._real = sfzbuilder.USER_FILES_DIR
+        sfzbuilder.USER_FILES_DIR = self.files
+        sfzbuilder.create_bank("Kit")
+
+    def tearDown(self):
+        sfzbuilder.USER_FILES_DIR = self._real
+        shutil.rmtree(self.tmp, ignore_errors=True)
+
+    def _build(self, source):
+        return sfzbuilder.build_bank("Kit", 36, [
+            {"sample": os.path.basename(source), "source": source,
+             "loop_mode": "one_shot"},
+        ])
+
+    def test_a_sample_under_user_files_is_copied(self):
+        source = os.path.join(self.files, "Loops", "kick.wav")
+        make_wav(source)
+        self.assertEqual(self._build(source)["count"], 1)
+        self.assertTrue(os.path.isfile(
+            os.path.join(sfzbuilder.bank_dir("Kit"), "kick.wav")))
+
+    def test_a_sample_from_anywhere_else_is_refused(self):
+        source = os.path.join(self.tmp, "elsewhere", "secret.wav")
+        make_wav(source)
+        with self.assertRaises(ValueError):
+            self._build(source)
+
+    def test_a_link_that_points_out_is_refused(self):
+        outside = os.path.join(self.tmp, "elsewhere", "secret.wav")
+        make_wav(outside)
+        link = os.path.join(self.files, "kick.wav")
+        os.symlink(outside, link)
+        with self.assertRaises(ValueError):
+            self._build(link)
+
+
+class TestExtensionsAgree(unittest.TestCase):
+    # model.js keeps its own copy of the list, because the panel removes the
+    # files the server would refuse before it sends a batch. The two lists have
+    # to say the same thing or the panel drops a file the server accepts.
+    def test_the_panel_knows_the_same_audio_types(self):
+        import re
+        here = os.path.dirname(os.path.abspath(__file__))
+        js = os.path.join(here, "..", "html", "js", "app", "sfzbuilder", "model.js")
+        with open(js) as fh:
+            text = fh.read()
+        body = re.search(r"export const AUDIO_EXTENSIONS = \[(.*?)\]", text, re.S)
+        self.assertIsNotNone(body, "AUDIO_EXTENSIONS is not in model.js any more")
+        found = tuple(re.findall(r"'([^']+)'", body.group(1)))
+        self.assertEqual(found, sfzbuilder.AUDIO_EXTENSIONS)
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..5e421a88
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,16 @@
+{
+  "//": "Type checking only. See docs/frontend-architecture.md rule 4.4. No build output.",
+  "compilerOptions": {
+    "target": "ES2022",
+    "module": "ES2022",
+    "moduleResolution": "bundler",
+    "lib": ["ES2022", "DOM", "DOM.Iterable"],
+    "allowJs": true,
+    "checkJs": true,
+    "noEmit": true,
+    "strict": true,
+    "noImplicitAny": false,
+    "skipLibCheck": true
+  },
+  "include": ["html/js/app/**/*.js"]
+}