From 183c3e2c856c41306b9754bd51ce05589cf1d773 Mon Sep 17 00:00:00 2001 From: Luis Fagundes Date: Thu, 9 Jul 2026 13:06:40 +0000 Subject: [PATCH 01/14] Add HTTP characterization test suite pytest + tornado AsyncHTTPTestCase harness running against the real web.Application with a faked audio backend (no JACK, mod-host or hardware needed). Covers /files/list behavior (type mapping, extension filtering, subfolder recursion, ordering, per-request freshness), the index page render, and the CORS header conventions. Co-Authored-By: Claude Fable 5 --- README.rst | 23 +++++++ pytest.ini | 3 + test-requirements.txt | 4 ++ test/base.py | 78 ++++++++++++++++++++++++ test/conftest.py | 129 ++++++++++++++++++++++++++++++++++++++++ test/test_cors.py | 40 +++++++++++++ test/test_files_list.py | 109 +++++++++++++++++++++++++++++++++ test/test_index.py | 42 +++++++++++++ 8 files changed, 428 insertions(+) create mode 100644 pytest.ini create mode 100644 test-requirements.txt create mode 100644 test/base.py create mode 100644 test/conftest.py create mode 100644 test/test_cors.py create mode 100644 test/test_files_list.py create mode 100644 test/test_index.py diff --git a/README.rst b/README.rst index 8c9ac1da5..029b5057a 100644 --- a/README.rst +++ b/README.rst @@ -58,3 +58,26 @@ And now you are ready to start the webserver:: Setting the environment variables is needed when developing on a PC. Open your browser and point to http://localhost:8888/. + +Test +---- + +The test suite in ``test/`` contains HTTP-level characterization tests (pytest + tornado's testing tools). +They run against a faked audio backend, so no JACK, mod-host or MOD hardware is needed. + +First complete the Install section above (virtualenv, python requirements and ``make -C utils`` — the tests +import the webserver, which requires ``utils/libmod_utils.so``). + +On Python 3.10 or newer, the pinned tornado 4.3 needs a one-time patch (see the note in ``requirements.txt``):: + + $ sed -i 's/collections.MutableMapping/collections.abc.MutableMapping/' modui-env/lib/python3.*/site-packages/tornado/httputil.py + +Install the test requirements and run the suite from the repository root:: + + $ source modui-env/bin/activate + $ pip3 install -r test-requirements.txt + $ pytest + +NOTE: ``test/hmi-protocol-integrationtest.py`` is not part of this suite — it is a standalone integration +test for the HMI serial protocol that requires JACK, mod-host and a serial device, and pytest does not +collect it. diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..3b84f2109 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = test +python_files = test_*.py diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 000000000..6957b218d --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,4 @@ +pytest + +# Python 3.12 removed ssl.match_hostname, which tornado 4.3 falls back to importing from this package +backports.ssl_match_hostname; python_version >= "3.12" diff --git a/test/base.py b/test/base.py new file mode 100644 index 000000000..958dd9a0d --- /dev/null +++ b/test/base.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Shared base class for layer-1 characterization tests. + +Uses tornado 4.3's tornado.testing.AsyncHTTPTestCase (unittest-based, plain +tornado-4 idioms -- no async/await) against the real module-level +mod.webserver.application. conftest.py has already set every MOD_* env var +before this module (or any test module) imports mod.webserver. +""" + +import json + +from tornado.testing import AsyncHTTPTestCase + +from conftest import _UserFilesHelper, _USER_FILES_DIR + + +class ModUITestCase(AsyncHTTPTestCase): + # Every test_*.py imports this class directly (`from base import + # ModUITestCase`) so it can subclass it -- which also puts it in each + # test module's globals. pytest's unittest integration collects *any* + # TestCase subclass it finds at module scope, regardless of name (the + # usual python_classes="Test*" filter does not apply to TestCase + # subclasses), so without this it would try to collect ModUITestCase + # itself as a test in every module that imports it and fail with + # "no attribute 'runTest'" (it defines no test_* methods of its own). + # Concrete subclasses below must set __test__ = True to opt back in, + # since __test__ is looked up via normal attribute inheritance. + __test__ = False + + def runTest(self): + # Never actually invoked as a real test (real test_* methods are + # collected and run individually by pytest). Exists only because + # modern pytest (9.x) instantiates every unittest.TestCase subclass + # once with methodName="runTest" during collection, to register + # fixture factories (_pytest.unittest.UnitTestCase.newinstance()). + # tornado 4.3's own AsyncTestCase.__init__ (tornado/testing.py) + # unconditionally does getattr(self, methodName) -- unlike stdlib + # unittest.TestCase, it does not special-case a missing "runTest" -- + # so without this method that probe instantiation raises + # AttributeError and collection fails for every test class. + pass + + def get_app(self): + # Imported lazily (not at module top-level) so that conftest.py's + # env-var setup is guaranteed to have already run before mod.webserver + # -- and mod.settings, which reads MOD_* env vars at import time -- + # is ever imported. + import mod.webserver + return mod.webserver.application + + def seed(self, relpath, content=b"data"): + """Write USER_FILES_DIR/ (creating parent dirs) for a test. + + Thin wrapper around conftest's user_files fixture helper, exposed + here because pytest fixture return values can't be injected as + parameters into unittest.TestCase test methods (which is what these + AsyncHTTPTestCase-derived tests are). The autouse `user_files` + fixture in conftest.py still handles wiping USER_FILES_DIR between + tests. + """ + return _UserFilesHelper(_USER_FILES_DIR).seed(relpath, content=content) + + def fetch_json(self, path, **kwargs): + """GET/POST/etc. `path` and decode the response body as JSON. + + Asserts the response declares a JSON content type, matching the + JsonRequestHandler.write() convention used across mod/webserver.py. + """ + response = self.fetch(path, **kwargs) + content_type = response.headers.get("Content-Type", "") + assert "application/json" in content_type, ( + "expected JSON content type for %s, got %r (body: %r)" + % (path, content_type, response.body) + ) + return response, json.loads(response.body.decode("utf-8")) diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 000000000..47848e23d --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +""" +Layer-1 characterization test harness bootstrap. + +This module runs at collection time, BEFORE any test module gets a chance to +``import mod.webserver`` (and transitively ``mod.settings``, which reads every +``MOD_*`` env var at import time). So all environment setup below is plain +module-level code, not a fixture -- fixtures would run too late. + +These tests exercise the real ``mod.webserver.application`` (the module-level +tornado ``web.Application`` built at import time) against a throwaway on-disk +tree. Nothing here calls ``mod.webserver.run()`` or ``prepare()`` -- those pull +in real hardware / mod-host / JACK setup that isn't available in this dev +environment. +""" + +import atexit +import os +import shutil +import sys +import tempfile + +import pytest + +# ---------------------------------------------------------------------------- +# 0. Fail fast if the native extension mod.webserver depends on isn't built. +# ---------------------------------------------------------------------------- + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +_LIBMOD_UTILS_SO = os.path.join(REPO_ROOT, "utils", "libmod_utils.so") + +if not os.path.isfile(_LIBMOD_UTILS_SO): + pytest.exit( + "libmod_utils.so not found at {0}.\n" + "Build it first: `make -C utils` (from the repo root). See " + "mod-ui/CLAUDE.md for one-time environment setup.".format(_LIBMOD_UTILS_SO), + returncode=1, + ) + +# ---------------------------------------------------------------------------- +# 1. Build a throwaway data/user-files tree and point every MOD_* env var +# that mod.settings reads at it, BEFORE mod.webserver (and mod.settings) +# is ever imported by a test module. +# ---------------------------------------------------------------------------- + +_TEST_ROOT = tempfile.mkdtemp(prefix="modui-test-") + +_DATA_DIR = os.path.join(_TEST_ROOT, "data") +_USER_FILES_DIR = os.path.join(_TEST_ROOT, "user-files") + +os.makedirs(_DATA_DIR, exist_ok=True) +os.makedirs(_USER_FILES_DIR, exist_ok=True) + +# Replicate the bits of mod.check_environment() that handlers rely on but +# that we never call (check_environment() only runs inside prepare()). +with open(os.path.join(_DATA_DIR, "banks.json"), "w") as fh: + fh.write("[]") +with open(os.path.join(_DATA_DIR, "favorites.json"), "w") as fh: + fh.write("[]") + +os.environ["MOD_DEV_ENVIRONMENT"] = "1" +os.environ["MOD_LOG"] = "0" +os.environ["MOD_DATA_DIR"] = _DATA_DIR +os.environ["MOD_USER_FILES_DIR"] = _USER_FILES_DIR +os.environ["MOD_HTML_DIR"] = os.path.join(REPO_ROOT, "html") +os.environ["MOD_DEFAULT_PEDALBOARD"] = os.path.join(REPO_ROOT, "default.pedalboard") + +# Make the repo importable the same way server.py does (test/ is not on +# sys.path by default under some pytest invocations). +if REPO_ROOT not in sys.path: + sys.path.insert(0, REPO_ROOT) + + +def _cleanup_test_root(): + shutil.rmtree(_TEST_ROOT, ignore_errors=True) + + +atexit.register(_cleanup_test_root) + +# ---------------------------------------------------------------------------- +# 2. Fixtures +# ---------------------------------------------------------------------------- + + +class _UserFilesHelper(object): + """Exposes USER_FILES_DIR plus a seed() helper to test modules.""" + + def __init__(self, root): + self.root = root + + def seed(self, relpath, content=b"data"): + """Create USER_FILES_DIR/ (and any parent dirs) with content.""" + fullpath = os.path.join(self.root, relpath) + os.makedirs(os.path.dirname(fullpath), exist_ok=True) + mode = "wb" if isinstance(content, bytes) else "w" + with open(fullpath, mode) as fh: + fh.write(content) + return fullpath + + +def _reset_user_files_dir(): + if os.path.isdir(_USER_FILES_DIR): + shutil.rmtree(_USER_FILES_DIR) + os.makedirs(_USER_FILES_DIR, exist_ok=True) + + +@pytest.fixture(autouse=True) +def user_files(): + """Function-scoped, autouse: wipe and recreate USER_FILES_DIR around + every test, so tests never see leftovers from one another. + + USER_FILES_DIR itself is fixed for the whole process (mod.settings reads + MOD_USER_FILES_DIR once, at import time), so we can't swap directories + per-test -- instead we clear its contents. + + autouse=True (rather than requiring tests to depend on it explicitly) + because our test classes are unittest.TestCase subclasses + (tornado.testing.AsyncHTTPTestCase) -- pytest applies autouse fixtures' + setup/teardown around unittest-style tests too, but does NOT support + injecting a fixture's return value as a test-method parameter for them. + Tests that need to seed files use ModUITestCase.seed() (test/base.py) + instead, which points at this same directory. + """ + _reset_user_files_dir() + yield _UserFilesHelper(_USER_FILES_DIR) + _reset_user_files_dir() diff --git a/test/test_cors.py b/test/test_cors.py new file mode 100644 index 000000000..983164924 --- /dev/null +++ b/test/test_cors.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for CORS headers. + +Pins the constraint that shapes the Tone3000 download design: ordinary +JsonRequestHandler routes (e.g. /files/list) send no +Access-Control-Allow-Origin at all, while RemoteRequestHandler subclasses +(e.g. Hello, at /hello) echo it back only for the mod.audio/moddevices.com +allow-list (mod/webserver.py:279-292). +""" + +from base import ModUITestCase + + +class TestFilesListCors(ModUITestCase): + __test__ = True + + def test_files_list_has_no_cors_header(self): + response = self.fetch("/files/list?types=bogus") + self.assertEqual(response.code, 200) + self.assertNotIn("Access-Control-Allow-Origin", response.headers) + + +class TestHelloCors(ModUITestCase): + __test__ = True + + def test_hello_echoes_allowed_origin(self): + response = self.fetch("/hello", headers={"Origin": "https://mod.audio"}) + self.assertEqual(response.code, 200) + self.assertEqual( + response.headers.get("Access-Control-Allow-Origin"), + "https://mod.audio", + ) + + def test_hello_omits_header_for_foreign_origin(self): + response = self.fetch("/hello", headers={"Origin": "https://evil.example"}) + self.assertEqual(response.code, 200) + self.assertNotIn("Access-Control-Allow-Origin", response.headers) diff --git a/test/test_files_list.py b/test/test_files_list.py new file mode 100644 index 000000000..866be6ad5 --- /dev/null +++ b/test/test_files_list.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for GET /files/list (mod/webserver.py:FilesList). + +These pin the *current* behavior of the pipeline the Tone3000 download +feature will rely on: a fresh os.walk per request, recursion into +subfolders (needed for zip-pack downloads), sort-by-fullname ordering, and +the exact JSON entry shape. +""" + +from base import ModUITestCase + + +class TestFilesList(ModUITestCase): + __test__ = True + + def test_missing_types_param_is_501(self): + response = self.fetch("/files/list") + self.assertEqual(response.code, 501) + + def test_unknown_type_is_empty_ok(self): + response, body = self.fetch_json("/files/list?types=bogus") + self.assertEqual(response.code, 200) + self.assertEqual(body, {"ok": True, "files": []}) + + def test_nammodel_missing_folder_is_empty_ok(self): + # No NAM Models folder exists at all under USER_FILES_DIR. + response, body = self.fetch_json("/files/list?types=nammodel") + self.assertEqual(response.code, 200) + self.assertEqual(body, {"ok": True, "files": []}) + + def test_nammodel_lists_nam_files_and_excludes_decoys(self): + self.seed("NAM Models/amp.nam") + self.seed("NAM Models/readme.txt") + self.seed("NAM Models/ir.wav") + + response, body = self.fetch_json("/files/list?types=nammodel") + self.assertEqual(response.code, 200) + self.assertTrue(body["ok"]) + + basenames = [f["basename"] for f in body["files"]] + self.assertEqual(basenames, ["amp.nam"]) + + entry = body["files"][0] + self.assertEqual(entry["filetype"], "nammodel") + self.assertTrue(entry["fullname"].endswith("NAM Models/amp.nam")) + self.assertTrue(entry["fullname"].startswith("/"), "fullname should be absolute") + + def test_nammodel_extension_match_is_case_insensitive(self): + self.seed("NAM Models/AMP.NAM") + + response, body = self.fetch_json("/files/list?types=nammodel") + self.assertEqual(response.code, 200) + basenames = [f["basename"] for f in body["files"]] + self.assertEqual(basenames, ["AMP.NAM"]) + + def test_nammodel_recurses_into_subfolders(self): + # Pins DP1: a zip pack unzipped into NAM Models// will list fine. + self.seed("NAM Models/top.nam") + self.seed("NAM Models/MyPack/clean.nam") + self.seed("NAM Models/MyPack/Nested/deep.nam") + + response, body = self.fetch_json("/files/list?types=nammodel") + self.assertEqual(response.code, 200) + basenames = sorted(f["basename"] for f in body["files"]) + self.assertEqual(basenames, ["clean.nam", "deep.nam", "top.nam"]) + + def test_nammodel_results_sorted_by_full_path(self): + # Pins DP2's findability baseline: ordering is decided by full path. + self.seed("NAM Models/zzz.nam") + self.seed("NAM Models/aaa.nam") + self.seed("NAM Models/MyPack/mmm.nam") + + response, body = self.fetch_json("/files/list?types=nammodel") + self.assertEqual(response.code, 200) + fullnames = [f["fullname"] for f in body["files"]] + self.assertEqual(fullnames, sorted(fullnames)) + + def test_nammodel_walk_is_fresh_every_request(self): + # Pins that /files/list is NOT cached server-side: a file dropped in + # between two requests is visible on the second request with no + # extra signal needed (the staleness the Tone3000 feature must + # solve is 100% client-side). + self.seed("NAM Models/first.nam") + + _, body1 = self.fetch_json("/files/list?types=nammodel") + self.assertEqual([f["basename"] for f in body1["files"]], ["first.nam"]) + + self.seed("NAM Models/second.nam") + + _, body2 = self.fetch_json("/files/list?types=nammodel") + self.assertEqual( + sorted(f["basename"] for f in body2["files"]), + ["first.nam", "second.nam"], + ) + + def test_multiple_types_are_unioned_with_own_filetype_tag(self): + self.seed("NAM Models/amp.nam") + self.seed("Reverb IRs/hall.wav") + + response, body = self.fetch_json("/files/list?types=nammodel,ir") + self.assertEqual(response.code, 200) + + by_basename = {f["basename"]: f for f in body["files"]} + self.assertEqual(set(by_basename), {"amp.nam", "hall.wav"}) + self.assertEqual(by_basename["amp.nam"]["filetype"], "nammodel") + self.assertEqual(by_basename["hall.wav"]["filetype"], "ir") diff --git a/test/test_index.py b/test/test_index.py new file mode 100644 index 000000000..17227b9a2 --- /dev/null +++ b/test/test_index.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for GET / (mod/webserver.py:TemplateHandler). + +This is the page a future Tone3000-tab edit will touch (the #main-menu +trigger-icon cluster). We pin the redirect-without-?v= behavior and, if the +dev-fake environment can render the full page without JACK/hardware, that +the rendered HTML still contains the anchor points the future edit needs. +""" + +from base import ModUITestCase + + +class TestIndexRedirect(ModUITestCase): + __test__ = True + + def test_bare_get_redirects_with_version_query_arg(self): + response = self.fetch("/", follow_redirects=False) + self.assertEqual(response.code, 302) + location = response.headers.get("Location", "") + self.assertIn("v=", location) + + +class TestIndexRender(ModUITestCase): + __test__ = True + + # TemplateHandler.get (mod/webserver.py) is a gen.coroutine that awaits + # SESSION.wait_for_hardware_if_needed -- if that never calls back under + # the dev-fake environment, this test would hang. self.fetch(...) + # applies AsyncTestCase's own timeout (ASYNC_TEST_TIMEOUT env var, + # default 5s) as a guard; conftest/CI can raise it if 5s is too tight. + + def test_get_with_version_renders_index_html(self): + response = self.fetch("/?v=1") + self.assertEqual(response.code, 200) + self.assertIn("text/html", response.headers.get("Content-Type", "")) + + body = response.body.decode("utf-8") + self.assertIn('id="main-menu"', body) + self.assertIn('id="mod-file-manager"', body) From ea0c290f17dbe0cb4a0f295378c78a9760312d18 Mon Sep 17 00:00:00 2001 From: Luis Fagundes Date: Thu, 9 Jul 2026 13:57:52 +0000 Subject: [PATCH 02/14] Refactor CORS tests and harden test sandbox CORS: split the hello tests into one allowed-origin base class with the origin as a class property (subclassed per allowed origin variant) and a separate foreign-origin class. Conftest: point MOD_USER_PEDALBOARDS_DIR/MOD_USER_PLUGINS_DIR at the temp tree (defaults are the real ~/.pedalboards and ~/.lv2), abort the run if any writable mod.settings path escapes the test root, and document the routes tests must never call. Co-Authored-By: Claude Fable 5 --- test/conftest.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++ test/test_cors.py | 29 +++++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 47848e23d..4d0dfe8f3 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -15,6 +15,25 @@ tree. Nothing here calls ``mod.webserver.run()`` or ``prepare()`` -- those pull in real hardware / mod-host / JACK setup that isn't available in this dev environment. + +NEVER-CALL LIST -- routes no test in this suite may fetch. They run +subprocesses, write outside the sandboxed temp tree, or block forever: + +- ``/system/cleanup`` deletes ``~/.pedalboards`` and ``~/.lv2`` +- ``/system/exechange`` reboot/systemctl/mod-backup subprocesses +- ``/update/download/``, ``/update/begin`` writes under /data, /tmp +- ``/controlchain/download/`` firmware download, subprocess mv to /tmp +- ``/switch_cpu_freq/`` writes /sys/devices/system/cpu +- ``/recording/*`` needs JACK; ``play/wait`` long-polls forever +- ``/pedalboard/pack_bundle``, ``/pedalboard/load_web``, + ``/pedalboard/factorycopy``, ``/pedalboard/image/generate`` subprocesses +- ``/effect/install``, ``/sdk/install``, ``/package/uninstall`` touch the + plugin dir via subprocess tar / rmtree +- ``/effect/list``, ``/effect/get*``, ``/effect/bulk``, ``/effect/add``, + ``/effect/image``, ``/effect/file``, ``/resources/(.*)?uri=`` dereference + the global lilv world, which is NULL until ``modtools.utils.init()`` runs + -- calling them without init SEGFAULTS the test process (see the phase-6 + spec in docs/ before touching these) """ import atexit @@ -50,9 +69,13 @@ _DATA_DIR = os.path.join(_TEST_ROOT, "data") _USER_FILES_DIR = os.path.join(_TEST_ROOT, "user-files") +_PEDALBOARDS_DIR = os.path.join(_TEST_ROOT, "pedalboards") +_PLUGINS_DIR = os.path.join(_TEST_ROOT, "lv2") os.makedirs(_DATA_DIR, exist_ok=True) os.makedirs(_USER_FILES_DIR, exist_ok=True) +os.makedirs(_PEDALBOARDS_DIR, exist_ok=True) +os.makedirs(_PLUGINS_DIR, exist_ok=True) # Replicate the bits of mod.check_environment() that handlers rely on but # that we never call (check_environment() only runs inside prepare()). @@ -67,6 +90,32 @@ os.environ["MOD_USER_FILES_DIR"] = _USER_FILES_DIR os.environ["MOD_HTML_DIR"] = os.path.join(REPO_ROOT, "html") os.environ["MOD_DEFAULT_PEDALBOARD"] = os.path.join(REPO_ROOT, "default.pedalboard") +# Without these two, mod.settings defaults LV2_PEDALBOARDS_DIR/LV2_PLUGIN_DIR +# to ~/.pedalboards and ~/.lv2 -- and pedalboard save/remove handlers would +# write into the REAL user home instead of the sandbox. +os.environ["MOD_USER_PEDALBOARDS_DIR"] = _PEDALBOARDS_DIR +os.environ["MOD_USER_PLUGINS_DIR"] = _PLUGINS_DIR + +# ---------------------------------------------------------------------------- +# 1b. Sandbox guard: every writable path mod.settings resolves must live +# under the throwaway test root. mod.settings only reads env vars and +# stdlib (importing it here is cheap and does NOT load libmod_utils.so), +# so this catches a broken/renamed MOD_* env var before any test can +# write outside the sandbox. +# ---------------------------------------------------------------------------- + +from mod import settings as _mod_settings # noqa: E402 (needs env set above) + +for _name in ("DATA_DIR", "USER_FILES_DIR", "LV2_PEDALBOARDS_DIR", "LV2_PLUGIN_DIR"): + _path = os.path.realpath(getattr(_mod_settings, _name)) + if not _path.startswith(os.path.realpath(_TEST_ROOT) + os.sep): + pytest.exit( + "SANDBOX GUARD: mod.settings.{0} resolves to {1}, which is outside " + "the test root {2}. Refusing to run -- tests could write into real " + "user/system directories. Check the MOD_* env vars set in " + "test/conftest.py against mod/settings.py.".format(_name, _path, _TEST_ROOT), + returncode=1, + ) # Make the repo importable the same way server.py does (test/ is not on # sys.path by default under some pytest invocations). diff --git a/test/test_cors.py b/test/test_cors.py index 983164924..9bfc5972d 100644 --- a/test/test_cors.py +++ b/test/test_cors.py @@ -23,17 +23,40 @@ def test_files_list_has_no_cors_header(self): self.assertNotIn("Access-Control-Allow-Origin", response.headers) -class TestHelloCors(ModUITestCase): +class TestHelloAllowedOriginCors(ModUITestCase): + """Base case for the allow-list: subclasses only change ``origin``.""" + __test__ = True + origin = "https://mod.audio" def test_hello_echoes_allowed_origin(self): - response = self.fetch("/hello", headers={"Origin": "https://mod.audio"}) + response = self.fetch("/hello", headers={"Origin": self.origin}) self.assertEqual(response.code, 200) self.assertEqual( response.headers.get("Access-Control-Allow-Origin"), - "https://mod.audio", + self.origin, ) + +class TestHelloAllowedOriginModdevicesCors(TestHelloAllowedOriginCors): + origin = "https://moddevices.com" + + +class TestHelloAllowedOriginPlainHttpCors(TestHelloAllowedOriginCors): + origin = "http://mod.audio" + + +class TestHelloAllowedOriginModAudioSubdomainCors(TestHelloAllowedOriginCors): + origin = "https://pedalboards.mod.audio" + + +class TestHelloAllowedOriginModdevicesSubdomainCors(TestHelloAllowedOriginCors): + origin = "https://cloud.moddevices.com" + + +class TestHelloForeignOriginCors(ModUITestCase): + __test__ = True + def test_hello_omits_header_for_foreign_origin(self): response = self.fetch("/hello", headers={"Origin": "https://evil.example"}) self.assertEqual(response.code, 200) From 26124bd804f805358cd1ef8ce2e10ba13859c2d8 Mon Sep 17 00:00:00 2001 From: Luis Fagundes Date: Thu, 9 Jul 2026 14:04:38 +0000 Subject: [PATCH 03/14] Add config and user-data characterization tests (phase 2) Covers favorites, config/set, save_user_id, tokens, banks, system info/prefs, hello, ping, template loaders, static-file and JSON handler header conventions. Pins current behavior, including 500s on /auth/nonce without device credentials, /tokens/save without expires_in_days, and missing templates, plus the banks.json rewrite side effect of GET /banks/. Co-Authored-By: Claude Fable 5 --- test/test_banks.py | 101 ++++++++++++++++++++++ test/test_config.py | 188 +++++++++++++++++++++++++++++++++++++++++ test/test_system.py | 90 ++++++++++++++++++++ test/test_templates.py | 110 ++++++++++++++++++++++++ test/test_tokens.py | 141 +++++++++++++++++++++++++++++++ 5 files changed, 630 insertions(+) create mode 100644 test/test_banks.py create mode 100644 test/test_config.py create mode 100644 test/test_system.py create mode 100644 test/test_templates.py create mode 100644 test/test_tokens.py diff --git a/test/test_banks.py b/test/test_banks.py new file mode 100644 index 000000000..2da07013a --- /dev/null +++ b/test/test_banks.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for /banks/save (BankSave) and /banks/ (BankLoad), +mod/webserver.py. + +conftest.py pre-seeds DATA_DIR/banks.json with "[]" (mimicking +check_environment(), which real prepare() calls but this sandbox never +does). Both handlers use mod.settings.USER_BANKS_JSON_FILE, so this module +snapshots/restores that file around every test. +""" + +import json +import os + +import pytest + +from base import ModUITestCase + + +@pytest.fixture(autouse=True) +def _snapshot_banks_json(): + from mod import settings + + with open(settings.USER_BANKS_JSON_FILE, "r") as fh: + before = fh.read() + + yield + + with open(settings.USER_BANKS_JSON_FILE, "w") as fh: + fh.write(before) + + +class TestBankLoadEmpty(ModUITestCase): + __test__ = True + + def test_load_with_empty_banks_json_returns_empty_list(self): + response, body = self.fetch_json("/banks/") + self.assertEqual(response.code, 200) + self.assertEqual(body, []) + + +class TestBankSave(ModUITestCase): + __test__ = True + + def test_save_writes_banks_json_and_returns_true(self): + banks = [{"title": "Bank 1", "pedalboards": []}] + response = self.fetch("/banks/save", method="POST", body=json.dumps(banks)) + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"true") + + from mod import settings + + with open(settings.USER_BANKS_JSON_FILE, "r") as fh: + on_disk = json.load(fh) + self.assertEqual(on_disk, banks) + + +class TestBankRoundTripNonexistentPedalboard(ModUITestCase): + __test__ = True + + def test_save_bank_referencing_missing_pedalboard_is_filtered_on_load(self): + # DP1/round-trip case from the spec: does BankLoad filter out a + # pedalboard whose bundle path doesn't exist in the sandboxed temp + # pedalboards dir (which is always empty here)? + banks = [ + { + "title": "Bank 1", + "pedalboards": [ + {"bundle": "/nonexistent/thing.pedalboard", "title": "Ghost"} + ], + } + ] + save_response = self.fetch( + "/banks/save", method="POST", body=json.dumps(banks) + ) + self.assertEqual(save_response.body, b"true") + + load_response, body = self.fetch_json("/banks/") + self.assertEqual(load_response.code, 200) + + # The bank itself survives, but its pedalboards list is filtered + # empty: list_banks() (mod/bank.py) drops any pedalboard whose + # 'bundle' path doesn't os.path.exists(), independent of whether it + # is "broken" -- and the temp pedalboards dir is always empty in + # this sandbox, so get_all_pedalboards() never repopulates it + # either. + self.assertEqual(len(body), 1) + self.assertEqual(body[0]["title"], "Bank 1") + self.assertEqual(body[0]["pedalboards"], []) + + # Observed side effect: list_banks() auto-rewrites banks.json to + # drop the now-filtered pedalboard entry as a side effect of a GET + # request (mod/bank.py:58-59, changed=True + shouldSave=True by + # default). So a plain GET /banks/ is not read-only on disk. + from mod import settings + + with open(settings.USER_BANKS_JSON_FILE, "r") as fh: + on_disk = json.load(fh) + self.assertEqual(on_disk[0]["pedalboards"], []) diff --git a/test/test_config.py b/test/test_config.py new file mode 100644 index 000000000..a898094fe --- /dev/null +++ b/test/test_config.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for the small config/user-data POST endpoints: +favorites (mod/webserver.py:FavoritesAdd/FavoritesRemove), /config/set +(SaveSingleConfigValue), /save_user_id/ (SaveUserId) and /auth/nonce +(AuthNonce). + +These all mutate either an in-process singleton (gState.favorites) or a +DATA_DIR JSON file (favorites.json, prefs.json, user-id.json). Since +conftest.py's autouse `user_files` fixture only resets USER_FILES_DIR, this +module snapshots/restores gState.favorites and the DATA_DIR files itself so +tests stay order-independent (per characterization-phase-2.md ground rule 5). +""" + +import json +import os + +import pytest + +from base import ModUITestCase + + +def _read_or_none(path): + if not os.path.exists(path): + return None + with open(path, "r") as fh: + return fh.read() + + +def _restore(path, original): + if original is None: + if os.path.exists(path): + os.remove(path) + else: + with open(path, "w") as fh: + fh.write(original) + + +@pytest.fixture(autouse=True) +def _snapshot_data_dir_state(): + import mod.webserver as webserver + from mod import settings + + favorites_snapshot = list(webserver.gState.favorites) + prefs_before = _read_or_none(settings.PREFERENCES_JSON_FILE) + user_id_before = _read_or_none(settings.USER_ID_JSON_FILE) + favorites_file_before = _read_or_none(settings.FAVORITES_JSON_FILE) + + yield + + webserver.gState.favorites[:] = favorites_snapshot + _restore(settings.PREFERENCES_JSON_FILE, prefs_before) + _restore(settings.USER_ID_JSON_FILE, user_id_before) + _restore(settings.FAVORITES_JSON_FILE, favorites_file_before) + + +class TestFavoritesAdd(ModUITestCase): + __test__ = True + + def test_add_writes_favorites_json_and_returns_true(self): + response = self.fetch( + "/favorites/add", method="POST", body="uri=http%3A%2F%2Fexample.org%2Ffx" + ) + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"true") + + from mod import settings + + with open(settings.FAVORITES_JSON_FILE, "r") as fh: + data = json.load(fh) + self.assertEqual(data, ["http://example.org/fx"]) + + def test_add_duplicate_returns_false_and_does_not_duplicate(self): + body = "uri=http%3A%2F%2Fexample.org%2Ffx" + first = self.fetch("/favorites/add", method="POST", body=body) + self.assertEqual(first.body, b"true") + + second = self.fetch("/favorites/add", method="POST", body=body) + self.assertEqual(second.code, 200) + self.assertEqual(second.body, b"false") + + from mod import settings + + with open(settings.FAVORITES_JSON_FILE, "r") as fh: + data = json.load(fh) + self.assertEqual(data, ["http://example.org/fx"]) + + def test_add_missing_uri_argument_is_400(self): + response = self.fetch("/favorites/add", method="POST", body="") + self.assertEqual(response.code, 400) + + +class TestFavoritesRemove(ModUITestCase): + __test__ = True + + def test_add_then_remove_empties_favorites_json(self): + body = "uri=http%3A%2F%2Fexample.org%2Ffx" + self.fetch("/favorites/add", method="POST", body=body) + + response = self.fetch("/favorites/remove", method="POST", body=body) + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"true") + + from mod import settings + + with open(settings.FAVORITES_JSON_FILE, "r") as fh: + data = json.load(fh) + self.assertEqual(data, []) + + def test_remove_unknown_uri_returns_false(self): + response = self.fetch( + "/favorites/remove", + method="POST", + body="uri=http%3A%2F%2Fnever-added.example%2Ffx", + ) + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"false") + + +class TestSaveSingleConfigValue(ModUITestCase): + __test__ = True + + def test_set_writes_prefs_json_and_returns_true(self): + response = self.fetch( + "/config/set", method="POST", body="key=some-key&value=some-value" + ) + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"true") + + from mod import settings + + with open(settings.PREFERENCES_JSON_FILE, "r") as fh: + data = json.load(fh) + # Values are stored verbatim as whatever get_argument() returned: + # tornado decodes form params to str, so both key and value land as + # plain strings in prefs.json (no int/bool coercion happens here). + self.assertEqual(data["some-key"], "some-value") + self.assertIsInstance(data["some-key"], str) + + def test_set_missing_key_argument_is_400(self): + response = self.fetch("/config/set", method="POST", body="value=x") + self.assertEqual(response.code, 400) + + +class TestSaveUserId(ModUITestCase): + __test__ = True + + def test_save_writes_user_id_json_and_returns_true(self): + response = self.fetch( + "/save_user_id/", + method="POST", + body="name=Ada&email=ada%40example.org", + ) + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"true") + + from mod import settings + + with open(settings.USER_ID_JSON_FILE, "r") as fh: + data = json.load(fh) + self.assertEqual(data, {"name": "Ada", "email": "ada@example.org"}) + + def test_save_missing_name_argument_is_400(self): + response = self.fetch( + "/save_user_id/", method="POST", body="email=ada%40example.org" + ) + self.assertEqual(response.code, 400) + + +class TestAuthNonce(ModUITestCase): + __test__ = True + + def test_auth_nonce_observed_behavior(self): + # Spec: "if the crypto `token` module is None -> {}; otherwise may + # error -- OBSERVE and pin actual." In this dev sandbox + # mod.communication.token imports fine (no exception at import + # time), so `token` is NOT None; but MOD_DEVICE_KEY / MOD_DEVICE_TAG + # are unset (conftest.py never sets them), so + # mod.communication.device.get_tag() raises "Missing device tag" + # once AuthNonce actually calls token.create_token_message(). That + # exception is uncaught by the handler, so tornado turns it into a + # 500. + response = self.fetch( + "/auth/nonce", method="POST", body=json.dumps({"nonce": "abc123"}) + ) + self.assertEqual(response.code, 500) diff --git a/test/test_system.py b/test/test_system.py new file mode 100644 index 000000000..7ab0058f2 --- /dev/null +++ b/test/test_system.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for /system/info (SystemInfo), /system/prefs +(SystemPreferences), /hello/ (Hello) and /ping/ (Ping), mod/webserver.py. + +None of these write anything -- they only read read-only system paths +(/etc/mod-release/system, /data/*) that don't exist on a plain dev machine, +so no snapshot/restore fixture is needed here. +""" + +from base import ModUITestCase + + +class TestSystemInfo(ModUITestCase): + __test__ = True + + def test_info_defaults_when_no_hardware_descriptor(self): + response, body = self.fetch_json("/system/info") + self.assertEqual(response.code, 200) + + # /etc/mod-hardware-descriptor.json and /etc/mod-release/system + # don't exist on this dev machine, so every hwdesc-derived field + # falls back to "Unknown" and sysdate falls back to "Unknown". + self.assertEqual(body["hwname"], "Unknown") + self.assertEqual(body["architecture"], "Unknown") + self.assertEqual(body["cpu"], "Unknown") + self.assertEqual(body["platform"], "Unknown") + self.assertEqual(body["bin_compat"], "Unknown") + self.assertEqual(body["model"], "Unknown") + self.assertEqual(body["sysdate"], "Unknown") + + self.assertIn("version", body["python"]) + self.assertIn("machine", body["uname"]) + self.assertIn("release", body["uname"]) + self.assertIn("sysname", body["uname"]) + self.assertIn("version", body["uname"]) + + +class TestSystemPreferences(ModUITestCase): + __test__ = True + + def test_prefs_defaults_when_no_data_files(self): + response, body = self.fetch_json("/system/prefs") + self.assertEqual(response.code, 200) + + # SystemPreferences reads hardcoded absolute "/data/..." paths (NOT + # mod.settings.DATA_DIR -- these are never sandboxed by conftest.py, + # but /data doesn't exist on this dev machine so every pref falls + # back to its default). + self.assertEqual(body["jack_buffer_size"], 128) + self.assertEqual(body["jack_mono_copy"], False) + self.assertEqual(body["jack_sync_mode"], False) + self.assertEqual(body["separate_spdif_outs"], False) + # "service_mod_peakmeter" is an OPTION_FILE_NOT_EXISTS pref, so it's + # True exactly when the disable-flag file is absent. + self.assertEqual(body["service_mod_peakmeter"], True) + self.assertEqual(body["service_mod_sdk"], False) + self.assertEqual(body["service_netmanager"], False) + self.assertEqual(body["autorestart_hmi"], False) + # bluetooth_name has no valdef override (defaults to None), so the + # key is present with a JSON null, not omitted. + self.assertIn("bluetooth_name", body) + self.assertIsNone(body["bluetooth_name"]) + + +class TestHello(ModUITestCase): + __test__ = True + + def test_hello_shape(self): + response, body = self.fetch_json("/hello/") + self.assertEqual(response.code, 200) + # No websocket ever connects in this test harness. + self.assertEqual(body["online"], False) + # IMAGE_VERSION is None in this dev sandbox (no /etc/mod-release/system). + self.assertIsNone(body["version"]) + + +class TestPing(ModUITestCase): + __test__ = True + + def test_ping_reports_hmi_offline(self): + response, body = self.fetch_json("/ping/") + self.assertEqual(response.code, 200) + # The fake HMI (mod.development.FakeHMI under MOD_DEV_ENVIRONMENT=1) + # is never .initialized, so web_ping() calls back False synchronously + # -- no 5s gen.with_timeout wait is actually exercised here. + self.assertEqual(body["ihm_online"], False) + self.assertEqual(body["ihm_time"], 0) diff --git a/test/test_templates.py b/test/test_templates.py new file mode 100644 index 000000000..94d9d97df --- /dev/null +++ b/test/test_templates.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for the template/static-file loaders and the +header conventions shared by every handler base class in mod/webserver.py: + +- TemplateLoader (/load_template/.html) +- BulkTemplateLoader (/js/templates.js) +- TimelessStaticFileHandler (any static file, e.g. /js/desktop.js) +- header pinning: JsonRequestHandler (no Date, no cache headers) vs + CachedJsonRequestHandler (Cache-Control + fixed Expires) + +None of these write anything, so no snapshot/restore fixture is needed. +""" + +from base import ModUITestCase + + +class TestTemplateLoader(ModUITestCase): + __test__ = True + + def test_loads_known_template_from_html_include(self): + response = self.fetch("/load_template/pedalboard.html") + self.assertEqual(response.code, 200) + self.assertIn("text/plain", response.headers.get("Content-Type", "")) + self.assertTrue(len(response.body) > 0) + + def test_unknown_template_is_500(self): + # TemplateLoader.get() open()s the file directly with no + # os.path.exists guard, so a missing template surfaces as an + # uncaught FileNotFoundError -> tornado 500, not a 404. + response = self.fetch("/load_template/does_not_exist.html") + self.assertEqual(response.code, 500) + + +class TestBulkTemplateLoader(ModUITestCase): + __test__ = True + + def test_bundles_html_include_into_templates_object(self): + response = self.fetch("/js/templates.js") + self.assertEqual(response.code, 200) + self.assertIn("text/javascript", response.headers.get("Content-Type", "")) + + body = response.body.decode("utf-8") + self.assertIn("TEMPLATES['pedalboard']", body) + + def test_has_cache_control_and_expires_headers(self): + # BulkTemplateLoader can't subclass CachedJsonRequestHandler (it's + # not JSON), so it sets the same two headers by hand -- pin that + # both routes converge on the same cache contract. + response = self.fetch("/js/templates.js") + self.assertEqual( + response.headers.get("Cache-Control"), "public, max-age=31536000" + ) + self.assertEqual( + response.headers.get("Expires"), "Mon, 31 Dec 2035 12:00:00 gmt" + ) + self.assertNotIn("Date", response.headers) + + +class TestTimelessStaticFileHandler(ModUITestCase): + __test__ = True + + def test_static_file_has_no_date_header(self): + response = self.fetch("/js/desktop.js") + self.assertEqual(response.code, 200) + self.assertNotIn("Date", response.headers) + self.assertEqual( + response.headers.get("Cache-Control"), "public, max-age=31536000" + ) + self.assertEqual( + response.headers.get("Expires"), "Mon, 31 Dec 2035 12:00:00 gmt" + ) + + +class TestJsonRequestHandlerHeaders(ModUITestCase): + __test__ = True + + def test_plain_json_route_has_no_date_and_no_cache_headers(self): + response = self.fetch("/files/list?types=bogus") + self.assertEqual(response.code, 200) + self.assertNotIn("Date", response.headers) + self.assertNotIn("Cache-Control", response.headers) + self.assertNotIn("Expires", response.headers) + + +class TestCachedJsonRequestHandlerHeaders(ModUITestCase): + __test__ = True + + def test_pedalboard_image_check_has_cache_headers(self): + # PedalboardImageCheck (CachedJsonRequestHandler) is filesystem-only + # (SESSION.screenshot_generator.check_screenshot just stats a + # thumbnail.png next to the given bundlepath) -- safe to call + # without lilv/JACK, unlike the /effect/* routes on the NEVER-CALL + # list. The other CachedJsonRequestHandler route, EffectGet, does + # need the global lilv world and is skipped (see NEVER-CALL list). + response, body = self.fetch_json( + "/pedalboard/image/check?bundlepath=/nonexistent/thing.pedalboard" + ) + self.assertEqual(response.code, 200) + self.assertEqual(body["status"], -1) + + self.assertEqual( + response.headers.get("Cache-Control"), "public, max-age=31536000" + ) + self.assertEqual( + response.headers.get("Expires"), "Mon, 31 Dec 2035 12:00:00 gmt" + ) + self.assertNotIn("Date", response.headers) diff --git a/test/test_tokens.py b/test/test_tokens.py new file mode 100644 index 000000000..10ea292c0 --- /dev/null +++ b/test/test_tokens.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for /tokens/save, /tokens/get, /tokens/delete +(mod/webserver.py:TokensSave/TokensGet/TokensDelete). + +These read/write DATA_DIR/tokens.conf directly (not via mod.settings, the +handlers build the path from the DATA_DIR global inline), so this module +snapshots/restores that file around every test to stay order-independent. +""" + +import json +import os + +import pytest + +from base import ModUITestCase + + +@pytest.fixture(autouse=True) +def _snapshot_tokens_conf(): + from mod import settings + + tokens_conf = os.path.join(settings.DATA_DIR, "tokens.conf") + existed = os.path.exists(tokens_conf) + before = None + if existed: + with open(tokens_conf, "r") as fh: + before = fh.read() + + yield + + if existed: + with open(tokens_conf, "w") as fh: + fh.write(before) + elif os.path.exists(tokens_conf): + os.remove(tokens_conf) + + +class TestTokensGetMissing(ModUITestCase): + __test__ = True + + def test_get_with_no_file_returns_ok_false(self): + response, body = self.fetch_json("/tokens/get") + self.assertEqual(response.code, 200) + self.assertEqual(body, {"ok": False}) + + +class TestTokensDeleteMissing(ModUITestCase): + __test__ = True + + def test_delete_with_no_file_is_a_noop_returning_true(self): + response = self.fetch("/tokens/delete") + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"true") + + +class TestTokensRoundTrip(ModUITestCase): + __test__ = True + + def _payload(self): + return { + "user_id": "u1", + "access_token": "at1", + "refresh_token": "rt1", + "expires_in_days": 30, + } + + def test_save_then_get_returns_saved_payload_minus_expires(self): + save_response = self.fetch( + "/tokens/save", method="POST", body=json.dumps(self._payload()) + ) + self.assertEqual(save_response.code, 200) + self.assertEqual(save_response.body, b"true") + + from mod import settings + + tokens_conf = os.path.join(settings.DATA_DIR, "tokens.conf") + with open(tokens_conf, "r") as fh: + on_disk = json.load(fh) + # TokensSave pops "expires_in_days" before writing to disk. + self.assertEqual( + on_disk, {"user_id": "u1", "access_token": "at1", "refresh_token": "rt1"} + ) + + get_response, body = self.fetch_json("/tokens/get") + self.assertEqual(get_response.code, 200) + self.assertEqual( + body, + { + "user_id": "u1", + "access_token": "at1", + "refresh_token": "rt1", + "ok": True, + }, + ) + + def test_save_missing_expires_in_days_is_500_and_does_not_write(self): + # TokensSave unconditionally data.pop("expires_in_days") before + # writing -- a payload that omits it raises an uncaught KeyError + # (500), and nothing is written to tokens.conf. + partial = {"user_id": "u1", "access_token": "at1", "refresh_token": "rt1"} + response = self.fetch( + "/tokens/save", method="POST", body=json.dumps(partial) + ) + self.assertEqual(response.code, 500) + + get_response, body = self.fetch_json("/tokens/get") + self.assertEqual(get_response.code, 200) + self.assertEqual(body, {"ok": False}) + + def test_get_missing_a_required_key_is_ok_false(self): + partial = {"user_id": "u1", "access_token": "at1", "expires_in_days": 30} + save_response = self.fetch( + "/tokens/save", method="POST", body=json.dumps(partial) + ) + self.assertEqual(save_response.body, b"true") + + response, body = self.fetch_json("/tokens/get") + self.assertEqual(response.code, 200) + self.assertEqual(body["ok"], False) + self.assertEqual(body["user_id"], "u1") + self.assertEqual(body["access_token"], "at1") + self.assertNotIn("refresh_token", body) + + def test_save_then_delete_then_get_is_ok_false_again(self): + self.fetch("/tokens/save", method="POST", body=json.dumps(self._payload())) + + delete_response = self.fetch("/tokens/delete") + self.assertEqual(delete_response.code, 200) + self.assertEqual(delete_response.body, b"true") + + from mod import settings + + tokens_conf = os.path.join(settings.DATA_DIR, "tokens.conf") + self.assertFalse(os.path.exists(tokens_conf)) + + get_response, body = self.fetch_json("/tokens/get") + self.assertEqual(get_response.code, 200) + self.assertEqual(body, {"ok": False}) From 504a4151526eb4f9f95d94685bf01b7407f23372 Mon Sep 17 00:00:00 2001 From: Luis Fagundes Date: Thu, 9 Jul 2026 14:19:40 +0000 Subject: [PATCH 04/14] Add pedalboard and snapshot characterization tests (phase 3) Save/info/remove/load_bundle round trip against the sandboxed pedalboards dir under the fake host, plus snapshot list/name/save/saveas behavior (including the Default snapshot seeded by SESSION.reset). Key finding recorded in the conftest never-call list: /pedalboard/list and /banks/ segfault (lilv_new_uri on the uninitialized global lilv world) as soon as a real pedalboard bundle exists on disk; both are only safe against an empty pedalboards dir, so the round trip verifies bundle presence via the filesystem and only lists once the dir is empty again. Conftest also gains an autouse fixture resetting the pedalboards dir between tests. Co-Authored-By: Claude Fable 5 --- test/conftest.py | 43 ++++++ test/test_pedalboards.py | 289 +++++++++++++++++++++++++++++++++++++++ test/test_snapshots.py | 158 +++++++++++++++++++++ 3 files changed, 490 insertions(+) create mode 100644 test/test_pedalboards.py create mode 100644 test/test_snapshots.py diff --git a/test/conftest.py b/test/conftest.py index 4d0dfe8f3..7033b54a8 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -34,6 +34,11 @@ the global lilv world, which is NULL until ``modtools.utils.init()`` runs -- calling them without init SEGFAULTS the test process (see the phase-6 spec in docs/ before touching these) +- ``/pedalboard/list``, ``/banks/`` while a real pedalboard bundle exists in + the sandbox pedalboards dir -- get_all_pedalboards SEGFAULTS parsing it + (NamespaceDefinitions::init / lilv_new_uri, needs the uninitialized global + lilv world). Both are safe against an EMPTY pedalboards dir; save/remove a + bundle within one test and only list after the dir is empty again """ import atexit @@ -176,3 +181,41 @@ def user_files(): _reset_user_files_dir() yield _UserFilesHelper(_USER_FILES_DIR) _reset_user_files_dir() + + +def _reset_pedalboards_dir(): + if os.path.isdir(_PEDALBOARDS_DIR): + shutil.rmtree(_PEDALBOARDS_DIR) + os.makedirs(_PEDALBOARDS_DIR, exist_ok=True) + + +@pytest.fixture(autouse=True) +def pedalboards_dir(): + """Function-scoped, autouse: wipe/recreate LV2_PEDALBOARDS_DIR and reset + the process-global SESSION around every test. + + Mirrors ``user_files`` above, for the same reason: LV2_PEDALBOARDS_DIR is + fixed for the whole process (mod.settings reads MOD_USER_PEDALBOARDS_DIR + once, at import time), so we clear its contents instead of swapping + directories. Phase 3 (docs/characterization-phase-3.md) is the first + phase whose tests write real pedalboard bundles to disk via + ``SESSION.host.save()`` (through ``POST /pedalboard/save``) and mutate + process-global state on ``SESSION.host`` (``pedalboard_path``, + ``pedalboard_snapshots``, ``current_pedalboard_snapshot_id``, ...) via + ``/pedalboard/load_bundle/`` and ``/snapshot/*``. Neither the on-disk + bundles nor that in-memory state may leak between tests or modules. + + ``SESSION.reset(callback)`` (mod/session.py) runs its callback + synchronously here: under ``MOD_DEV_ENVIRONMENT=1`` the FakeHMI is never + "initialized" (see module docstring), so ``Session.reset`` takes its + synchronous branch straight to ``Host.reset``, and ``FakeHost`` (see + ``mod/development.py``) invokes every ``send_notmodified``/ + ``send_modified`` callback immediately with ``True`` -- no real + mod-host, no IOLoop pump required. + """ + _reset_pedalboards_dir() + from mod.webserver import SESSION + SESSION.reset(lambda ok: None) + yield + SESSION.reset(lambda ok: None) + _reset_pedalboards_dir() diff --git a/test/test_pedalboards.py b/test/test_pedalboards.py new file mode 100644 index 000000000..6252d77e2 --- /dev/null +++ b/test/test_pedalboards.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for the pedalboard lifecycle: list, save, info, +remove, load_bundle (mod/webserver.py: PedalboardList, PedalboardSave, +PedalboardInfo, PedalboardRemove, PedalboardLoadBundle). + +CRITICAL, spec-overriding finding (see docs/characterization-phase-3.md, +which claimed /pedalboard/list and /banks/ are "self-contained lilv calls" +and safe): **GET /pedalboard/list and GET /banks/ SEGFAULT the whole test +process the moment the sandboxed pedalboards dir contains a real, on-disk +pedalboard bundle** (one written by POST /pedalboard/save). Root cause, +confirmed with faulthandler/gdb: modtools.utils.get_all_pedalboards() +(mod/webserver.py:1316 for PedalboardList, :1685 for BankLoad) invalidates +its Python-side cache and calls into utils.get_all_pedalboards() (the +libmod_utils.so C extension), which crashes inside +``NamespaceDefinitions::init(LilvWorldImpl*)`` -> ``lilv_new_uri()`` while +building its lilv world over a *real* bundle -- reproduced identically via +both PedalboardList and BankLoad, deterministically, every time, and +independent of how many times /pedalboard/list was called before (repeated +calls against an *empty* dir never crash; a *single* call against a dir +containing one real saved bundle crashes every time). By contrast, +GET /pedalboard/info/ (single-bundle parse via get_pedalboard_info(), a +different C entry point) and GET /pedalboard/remove/ are safe with a real +bundle present -- confirmed by direct probing with faulthandler enabled. + +Consequence for this suite: no test here calls GET /pedalboard/list or +GET /banks/ while a real bundle exists in the sandboxed pedalboards dir. +The "list now includes TestBoard" step from the phase-3 spec's core +round-trip is NOT executed as a live HTTP call; the bundle's presence is +instead verified directly on disk (os.path), which is what a real GET +/pedalboard/list would enumerate. The bundle is always removed via +GET /pedalboard/remove/ before any subsequent GET /pedalboard/list call in +the same test. + +The autouse `pedalboards_dir` fixture (test/conftest.py) wipes +LV2_PEDALBOARDS_DIR and resets SESSION around every test, so tests are +order-independent regarding on-disk bundles and SESSION.host state. +""" + +import os +import urllib.parse + +from base import ModUITestCase + + +def _quote(bundlepath): + return urllib.parse.quote(bundlepath, safe="") + + +class TestPedalboardListBaseline(ModUITestCase): + __test__ = True + + def test_list_on_empty_sandbox_is_empty_list(self): + # Safe: the sandboxed pedalboards dir is always empty here (no save + # has happened yet in this test). + response, body = self.fetch_json("/pedalboard/list") + self.assertEqual(response.code, 200) + self.assertEqual(body, []) + + +class TestPedalboardSave(ModUITestCase): + __test__ = True + + def test_save_missing_title_is_400(self): + response = self.fetch("/pedalboard/save", method="POST", body="asNew=1") + self.assertEqual(response.code, 400) + + def test_save_missing_asnew_is_400(self): + response = self.fetch("/pedalboard/save", method="POST", body="title=X") + self.assertEqual(response.code, 400) + + def test_save_creates_bundle_on_disk_with_expected_json_shape(self): + import mod.settings as settings + + body = urllib.parse.urlencode({"title": "TestBoard", "asNew": "1"}) + response, payload = self.fetch_json( + "/pedalboard/save", method="POST", body=body + ) + self.assertEqual(response.code, 200) + self.assertEqual( + payload, {"ok": True, "bundlepath": payload["bundlepath"], "title": "TestBoard"} + ) + + bundlepath = payload["bundlepath"] + self.assertTrue(bundlepath.startswith(settings.LV2_PEDALBOARDS_DIR)) + self.assertTrue(bundlepath.endswith(".pedalboard")) + self.assertTrue(os.path.isdir(bundlepath)) + + # A .ttl bundle got written (host.save_state_to_ttl), NOT observed + # through /pedalboard/list (see module docstring -- that would + # segfault) but directly on disk, which is exactly what a real + # /pedalboard/list GET would enumerate. + entries = os.listdir(bundlepath) + self.assertTrue(any(name.endswith(".ttl") for name in entries)) + self.assertIn("manifest.ttl", entries) + + # cleanup: remove before this test method returns, so no real + # bundle is left for the next test even though the autouse fixture + # would also wipe it. + remove_response = self.fetch( + "/pedalboard/remove/?bundlepath=" + _quote(bundlepath) + ) + self.assertEqual(remove_response.body, b"true") + + def test_save_asnew_0_over_fresh_session_behaves_like_asnew_1(self): + # No pedalboard was ever loaded/saved in this session yet, so + # host.pedalboard_path is "" -- host.save()'s "save over existing" + # branch requires a truthy, on-disk, sandbox-rooted pedalboard_path, + # so asNew=0 here takes the same "save new" branch as asNew=1. + body = urllib.parse.urlencode({"title": "FreshBoard", "asNew": "0"}) + response, payload = self.fetch_json( + "/pedalboard/save", method="POST", body=body + ) + self.assertEqual(response.code, 200) + self.assertTrue(payload["ok"]) + self.assertTrue(os.path.isdir(payload["bundlepath"])) + + self.fetch("/pedalboard/remove/?bundlepath=" + _quote(payload["bundlepath"])) + + def test_save_asnew_0_after_asnew_1_overwrites_same_bundle(self): + body1 = urllib.parse.urlencode({"title": "TestBoard", "asNew": "1"}) + _, payload1 = self.fetch_json("/pedalboard/save", method="POST", body=body1) + bundlepath1 = payload1["bundlepath"] + + body2 = urllib.parse.urlencode({"title": "TestBoard", "asNew": "0"}) + _, payload2 = self.fetch_json("/pedalboard/save", method="POST", body=body2) + bundlepath2 = payload2["bundlepath"] + + self.assertEqual(bundlepath1, bundlepath2) + + import mod.settings as settings + + self.assertEqual(os.listdir(settings.LV2_PEDALBOARDS_DIR), [os.path.basename(bundlepath1)]) + + self.fetch("/pedalboard/remove/?bundlepath=" + _quote(bundlepath1)) + + +class TestPedalboardInfo(ModUITestCase): + __test__ = True + + def test_info_on_bogus_bundlepath_is_500(self): + # get_pedalboard_info() raises a bare Exception on failure + # (modtools/utils.py); PedalboardInfo.get() does not catch it, so it + # surfaces as tornado's generic uncaught-exception 500 HTML page -- + # NOT a JsonRequestHandler JSON error body. + response = self.fetch( + "/pedalboard/info/?bundlepath=" + _quote("/nonexistent/thing.pedalboard") + ) + self.assertEqual(response.code, 500) + self.assertIn("text/html", response.headers.get("Content-Type", "")) + + def test_info_on_real_bundle_matches_saved_title(self): + save_body = urllib.parse.urlencode({"title": "TestBoard", "asNew": "1"}) + _, save_payload = self.fetch_json( + "/pedalboard/save", method="POST", body=save_body + ) + bundlepath = save_payload["bundlepath"] + + response, info = self.fetch_json( + "/pedalboard/info/?bundlepath=" + _quote(bundlepath) + ) + self.assertEqual(response.code, 200) + self.assertEqual(info["title"], "TestBoard") + self.assertIn("plugins", info) + self.assertIn("width", info) + self.assertIn("height", info) + + self.fetch("/pedalboard/remove/?bundlepath=" + _quote(bundlepath)) + + +class TestPedalboardRemove(ModUITestCase): + __test__ = True + + def test_remove_bogus_bundlepath_returns_false(self): + response = self.fetch( + "/pedalboard/remove/?bundlepath=" + _quote("/nonexistent/thing.pedalboard") + ) + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"false") + + def test_remove_nonexistent_path_outside_sandbox_returns_false_and_touches_nothing(self): + # PedalboardRemove gates on os.path.exists(bundlepath) before ever + # calling shutil.rmtree -- a nonexistent path outside the sandbox is + # therefore a safe no-op observation, not a real escape attempt. + outside_path = "/tmp/modui-characterization-does-not-exist.pedalboard" + self.assertFalse(os.path.exists(outside_path)) + + response = self.fetch("/pedalboard/remove/?bundlepath=" + _quote(outside_path)) + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"false") + + +class TestPedalboardCoreRoundTrip(ModUITestCase): + __test__ = True + + def test_save_info_remove_round_trip(self): + """The highest-value test (spec's "core round-trip"), adapted for + the /pedalboard/list segfault documented in the module docstring: + the bundle's presence/absence is asserted via the filesystem and + via /pedalboard/info/, never via a live /pedalboard/list call while + the bundle exists. + """ + import mod.settings as settings + + # 1. Baseline: safe, dir is empty (autouse fixture guarantees this). + response, body = self.fetch_json("/pedalboard/list") + self.assertEqual(body, []) + + # 2. Save. + save_body = urllib.parse.urlencode({"title": "TestBoard", "asNew": "1"}) + save_response, save_payload = self.fetch_json( + "/pedalboard/save", method="POST", body=save_body + ) + self.assertEqual(save_response.code, 200) + self.assertTrue(save_payload["ok"]) + bundlepath = save_payload["bundlepath"] + self.assertEqual(save_payload["title"], "TestBoard") + + # 3. "List now includes TestBoard" -- verified on disk (this is + # exactly what get_all_pedalboards() would scan), NOT via a live + # GET /pedalboard/list call, which segfaults with a real bundle + # present (see module docstring). + self.assertTrue(os.path.isdir(bundlepath)) + self.assertEqual( + os.listdir(settings.LV2_PEDALBOARDS_DIR), + [os.path.basename(bundlepath)], + ) + + # 4. Info. + info_response, info = self.fetch_json( + "/pedalboard/info/?bundlepath=" + _quote(bundlepath) + ) + self.assertEqual(info_response.code, 200) + self.assertEqual(info["title"], "TestBoard") + + # 5. Remove. + remove_response = self.fetch( + "/pedalboard/remove/?bundlepath=" + _quote(bundlepath) + ) + self.assertEqual(remove_response.code, 200) + self.assertEqual(remove_response.body, b"true") + self.assertFalse(os.path.exists(bundlepath)) + + # Now the dir is empty again, so a live /pedalboard/list call is + # safe once more (see module docstring: only a NON-empty dir + # segfaults) and should be back to baseline. + final_response, final_body = self.fetch_json("/pedalboard/list") + self.assertEqual(final_response.code, 200) + self.assertEqual(final_body, []) + + +class TestPedalboardLoadBundle(ModUITestCase): + __test__ = True + + def test_load_bundle_on_bogus_bundlepath_returns_ok_false(self): + body = urllib.parse.urlencode( + {"bundlepath": "/nonexistent/thing.pedalboard", "isDefault": "0"} + ) + response, payload = self.fetch_json( + "/pedalboard/load_bundle/", method="POST", body=body + ) + self.assertEqual(response.code, 200) + self.assertEqual(payload, {"ok": False, "name": ""}) + + def test_load_bundle_on_real_saved_bundle_returns_ok_true_with_name(self): + save_body = urllib.parse.urlencode({"title": "TestBoard", "asNew": "1"}) + _, save_payload = self.fetch_json( + "/pedalboard/save", method="POST", body=save_body + ) + bundlepath = save_payload["bundlepath"] + + load_body = urllib.parse.urlencode( + {"bundlepath": bundlepath, "isDefault": "0"} + ) + response, payload = self.fetch_json( + "/pedalboard/load_bundle/", method="POST", body=load_body + ) + self.assertEqual(response.code, 200) + self.assertEqual(payload, {"ok": True, "name": "TestBoard"}) + + # Reset SESSION before cleanup, per phase-3 spec's order-dependence + # warning: SESSION.host is a process-global singleton, so a test + # that loads a pedalboard must reset afterwards. + reset_response = self.fetch("/reset/") + self.assertEqual(reset_response.code, 200) + + self.fetch("/pedalboard/remove/?bundlepath=" + _quote(bundlepath)) diff --git a/test/test_snapshots.py b/test/test_snapshots.py new file mode 100644 index 000000000..566a9e3a3 --- /dev/null +++ b/test/test_snapshots.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for the snapshot endpoints (mod/webserver.py: +SnapshotList, SnapshotName, SnapshotSave, SnapshotSaveAs). + +Snapshots live entirely on SESSION.host (Python lists/dicts) -- unlike the +pedalboard endpoints in test_pedalboards.py, nothing here calls into lilv, +so there is no segfault risk (confirmed by direct probing). + +SESSION.host is a process-global singleton (mod/session.py), and these +handlers mutate it (pedalboard_snapshots, current_pedalboard_snapshot_id). +The autouse `pedalboards_dir` fixture (test/conftest.py) calls +SESSION.reset() before and after every test, so each test method here +starts from a known-fresh state: pedalboard_snapshots == [], and +current_pedalboard_snapshot_id == -1 (mod/host.py Host.__init__ defaults; +SESSION.reset() restores current_pedalboard_snapshot_id to 0 with a single +"Default" snapshot via host.snapshot_clear() -- see the "after /reset" +tests below, which pin that distinction). +""" + +import urllib.parse + +from base import ModUITestCase + + +class TestSnapshotFreshSession(ModUITestCase): + __test__ = True + + def test_list_after_fixture_reset_has_one_default_entry(self): + # NOT {} -- host.snapshot_clear() (invoked by SESSION.reset(), which + # the autouse pedalboards_dir fixture runs before every test) seeds + # exactly one "Default" snapshot at index 0 (host.py:3052-3053). + # See TestSnapshotSaveWithoutAnyReset below for the true "nothing at + # all" shape ({}), reached only by clearing the list by hand. + response, body = self.fetch_json("/snapshot/list") + self.assertEqual(response.code, 200) + self.assertEqual(body, {"0": "Default"}) + + def test_name_with_default_id_after_reset(self): + response, body = self.fetch_json("/snapshot/name?id=0") + self.assertEqual(response.code, 200) + self.assertEqual(body, {"ok": True, "name": "Default"}) + + def test_name_with_out_of_range_id_falls_back_to_default_name(self): + # snapshot_name(idx) returns None for an out-of-range idx, and the + # handler falls back to DEFAULT_SNAPSHOT_NAME ("Default") -- so this + # is NOT a 404/error shape, it's indistinguishable in its "ok": True + # shape from a real snapshot named "Default". + response, body = self.fetch_json("/snapshot/name?id=99") + self.assertEqual(response.code, 200) + self.assertEqual(body, {"ok": True, "name": "Default"}) + + def test_save_after_reset_succeeds_since_reset_seeds_snapshot_0(self): + # SESSION.reset() leaves current_pedalboard_snapshot_id == 0 with a + # real snapshot at index 0 (see host.py snapshot_clear()), so + # SnapshotSave succeeds here -- contrast with + # TestSnapshotSaveWithoutAnyReset below, which pins the *true* + # "nothing to save" shape (current_pedalboard_snapshot_id == -1). + response = self.fetch("/snapshot/save", method="POST", body="") + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"true") + + def test_saveas_creates_new_snapshot_and_appears_in_list(self): + response, payload = self.fetch_json("/snapshot/saveas?title=Foo") + self.assertEqual(response.code, 200) + self.assertEqual(payload["ok"], True) + self.assertEqual(payload["title"], "Foo") + new_id = payload["id"] + + list_response, list_body = self.fetch_json("/snapshot/list") + self.assertEqual(list_response.code, 200) + self.assertEqual(list_body[str(new_id)], "Foo") + # the pre-existing "Default" snapshot (seeded by SESSION.reset()) + # is still present alongside the new one. + self.assertIn("0", list_body) + + +class TestSnapshotSaveWithoutAnyReset(ModUITestCase): + __test__ = True + + def test_save_returns_false_when_current_snapshot_id_is_negative_one(self): + # Pins the "truly nothing to save" shape from a session that has + # never had snapshot_clear() run at all: Host.__init__ (mod/host.py) + # defaults current_pedalboard_snapshot_id to -1, and snapshot_save() + # returns False whenever that index is out of range. We reach this + # state by resetting the underlying attribute directly (the HTTP + # surface has no route that produces it, since even GET /reset + # seeds a "Default" snapshot at id 0 -- see module docstring). + from mod.webserver import SESSION + + SESSION.host.pedalboard_snapshots = [] + SESSION.host.current_pedalboard_snapshot_id = -1 + + response = self.fetch("/snapshot/save", method="POST", body="") + self.assertEqual(response.code, 200) + self.assertEqual(response.body, b"false") + + def test_list_is_empty_dict_when_snapshots_list_is_empty(self): + from mod.webserver import SESSION + + SESSION.host.pedalboard_snapshots = [] + + response, body = self.fetch_json("/snapshot/list") + self.assertEqual(response.code, 200) + self.assertEqual(body, {}) + + +class TestSnapshotRoundTripWithRealPedalboard(ModUITestCase): + __test__ = True + + def test_saveas_and_list_after_loading_a_real_pedalboard(self): + # Save a real bundle, load it back (this reseeds + # pedalboard_snapshots with a single "Default" snapshot via + # host.load() -> save_state_snapshots()/snapshots.json handling), + # then exercise snapshot/saveas + snapshot/list against it. + save_body = urllib.parse.urlencode({"title": "TestBoard", "asNew": "1"}) + _, save_payload = self.fetch_json( + "/pedalboard/save", method="POST", body=save_body + ) + bundlepath = save_payload["bundlepath"] + + load_body = urllib.parse.urlencode( + {"bundlepath": bundlepath, "isDefault": "0"} + ) + load_response, load_payload = self.fetch_json( + "/pedalboard/load_bundle/", method="POST", body=load_body + ) + self.assertEqual(load_response.code, 200) + self.assertTrue(load_payload["ok"]) + + list_response, list_body = self.fetch_json("/snapshot/list") + self.assertEqual(list_response.code, 200) + self.assertEqual(list_body, {"0": "Default"}) + + saveas_response, saveas_payload = self.fetch_json( + "/snapshot/saveas?title=Verse" + ) + self.assertEqual(saveas_response.code, 200) + self.assertEqual(saveas_payload["ok"], True) + self.assertEqual(saveas_payload["title"], "Verse") + + list_response2, list_body2 = self.fetch_json("/snapshot/list") + self.assertEqual(list_body2, {"0": "Default", "1": "Verse"}) + + # Order-dependence warning (phase-3 spec): reset SESSION.host before + # removing the bundle, so later tests see a clean session -- not + # via a live /pedalboard/list call (segfaults with a real bundle + # present, see test_pedalboards.py's module docstring), so we go + # straight to /reset/ then remove. + reset_response = self.fetch("/reset/") + self.assertEqual(reset_response.code, 200) + + self.fetch( + "/pedalboard/remove/?bundlepath=" + + urllib.parse.quote(bundlepath, safe="") + ) From bb2b10385c584a23529b9d473d8fc91d9b0b3cc9 Mon Sep 17 00:00:00 2001 From: Luis Fagundes Date: Thu, 9 Jul 2026 14:27:35 +0000 Subject: [PATCH 05/14] Add fake-host command characterization tests (phase 4) Pins webserver handler behavior under FakeHost/FakeHMI (not the production mod-host protocol): connect/disconnect always true, parameter set short-circuits on uninitialized HMI, reset, buffersize {ok:false,size:0} in dev, xruns, midi device shapes (500 on missing body keys), truebypass false, transport sync modes, cv port add 500 without plugin instances. /effect/remove/ is added to the never-call list: for any unregistered instance the KeyError from mapper.get_id_without_creating is swallowed by the gen.coroutine future in Host.remove_plugin, the callback never fires, and the request hangs forever. Co-Authored-By: Claude Fable 5 --- test/conftest.py | 14 ++ test/test_host_commands.py | 296 +++++++++++++++++++++++++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 test/test_host_commands.py diff --git a/test/conftest.py b/test/conftest.py index 7033b54a8..d7f489118 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -39,6 +39,20 @@ (NamespaceDefinitions::init / lilv_new_uri, needs the uninitialized global lilv world). Both are safe against an EMPTY pedalboards dir; save/remove a bundle within one test and only list after the dir is empty again +- ``/effect/remove/`` HANGS (no crash, no timeout server-side -- + the response is simply never sent) for any instance name not already + registered in ``SESSION.host.mapper``, which is every instance name in + this sandbox (``POST /effect/add`` is itself banned above, so no instance + can ever be registered). Root cause: ``Host.remove_plugin`` + (``mod/host.py:2604``, ``@gen.coroutine``) calls + ``self.mapper.get_id_without_creating(instance)`` *before* its own + try/except KeyError guard around ``self.plugins.pop(...)`` a few lines + down -- the KeyError from the lookup itself is swallowed into the + coroutine's Future instead of propagating, so the handler's + ``callback(False)`` is never reached and ``gen.Task`` in ``EffectRemove`` + never resolves. Confirmed by probing with a 4s client-side + ``request_timeout``: HTTP 599 (client timeout), not a fast response. See + ``test/test_host_commands.py`` module docstring (phase 4). """ import atexit diff --git a/test/test_host_commands.py b/test/test_host_commands.py new file mode 100644 index 000000000..27ca1cbab --- /dev/null +++ b/test/test_host_commands.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for the handlers that route into +SESSION.host/SESSION.hmi (mod/webserver.py: EffectConnect, EffectDisconnect, +EffectParameterSet, EffectParameterAddress, DashboardClean, SetBufferSize, +ResetXruns, JackGetMidiDevices, JackSetMidiDevices, TrueBypass, +PedalboardTransportSetSyncMode, PedalboardCvAddressingPluginPortAdd). + +Honest caveat: these tests pin the behavior of the mod/webserver.py handler +code under FakeHost/FakeHMI (mod/development.py), NOT the production +mod-host protocol. They are a regression net for webserver.py edits, +nothing more. + +CRITICAL, spec-overriding finding: GET /effect/remove/ HANGS (does +not error, does not time out server-side -- the HTTP response is simply +never sent) when was never registered with SESSION.host.mapper, +which is true of *every* instance name in this sandbox since we cannot call +POST /effect/add (banned -- dereferences the uninitialized global lilv +world, see test/conftest.py NEVER-CALL LIST). Root cause, confirmed by +direct probing with a 4s client-side request_timeout (got HTTP 599 after +the timeout, not a fast response): Host.remove_plugin (mod/host.py:2604, +decorated @gen.coroutine) calls self.mapper.get_id_without_creating(instance) +*before* its own try/except KeyError guard around self.plugins.pop(...). A +nonexistent instance means that lookup itself raises KeyError, which the +@gen.coroutine machinery captures into the returned (never awaited-on) +Future instead of propagating synchronously -- so the handler's +`callback(False)` line is never reached, gen.Task in EffectRemove never +resolves, and the request hangs until the HTTP client's own timeout. This +class does not call GET /effect/remove/* at all; the route has been added to +the test/conftest.py NEVER-CALL LIST with this reasoning. + +Every test below that mutates SESSION.host state relies on the autouse +`pedalboards_dir` fixture (test/conftest.py) to call SESSION.reset() before +and after each test method, EXCEPT sync-mode: SESSION.reset() does not touch +SESSION.host.profile, so the sync-mode test explicitly restores the default +("/none") at the end. +""" + +import json +import urllib.parse + +from base import ModUITestCase + + +class TestEffectConnectDisconnect(ModUITestCase): + __test__ = True + + def test_connect_between_system_ports_returns_true_and_is_idempotent(self): + # "/graph/capture_1" and "/graph/playback_1" are hardware-port + # shortcuts handled entirely inside Host._fix_host_connection_port + # (mod/host.py) without touching the plugin mapper, so they are safe + # "syntactically valid but nonexistent instance" stand-ins per the + # spec's guidance. FakeHost.send_modified (mod/development.py) + # invokes its callback immediately with True regardless of whether + # the ports are real JACK ports -- there is no validation under the + # fakes. + response, body = self.fetch_json( + "/effect/connect/graph/capture_1,/graph/playback_1" + ) + self.assertEqual(response.code, 200) + self.assertIs(body, True) + + # Second call: (port_from, port_to) is now already in + # self.connections, so Host.connect short-circuits to callback(True) + # without going through send_modified again -- still True. + response2, body2 = self.fetch_json( + "/effect/connect/graph/capture_1,/graph/playback_1" + ) + self.assertEqual(response2.code, 200) + self.assertIs(body2, True) + + # Clean up: disconnect so SESSION.host.connections is restored to + # empty (belt-and-braces -- the autouse fixture would clear it too). + response3, body3 = self.fetch_json( + "/effect/disconnect/graph/capture_1,/graph/playback_1" + ) + self.assertEqual(response3.code, 200) + self.assertIs(body3, True) + + def test_disconnect_with_no_active_connections_returns_true(self): + # Spec guessed this might be `false`. Observed: Host.disconnect's + # inner host_callback(ok) *always* calls callback(True) regardless + # of ok (mod/host.py:3422-3436, "always return true" per its own + # comment) -- even the len(self.connections) == 0 short-circuit + # path (host_callback(False)) ends up reporting True to the client. + response, body = self.fetch_json( + "/effect/disconnect/graph/capture_1,/graph/playback_1" + ) + self.assertEqual(response.code, 200) + self.assertIs(body, True) + + +class TestEffectParameterSet(ModUITestCase): + __test__ = True + + def test_parameter_set_short_circuits_true_when_hmi_uninitialized(self): + # FakeHMI.initialized is always False (mod/development.py), so + # EffectParameterSet.post's `if not SESSION.hmi.initialized: + # self.write(True); return` guard fires before the request body is + # even parsed -- an empty body is fine. + response, body = self.fetch_json( + "/effect/parameter/set/", method="POST", body=b"" + ) + self.assertEqual(response.code, 200) + self.assertIs(body, True) + + +class TestEffectParameterAddress(ModUITestCase): + __test__ = True + + def test_address_nonexistent_instance_returns_false(self): + # Unlike EffectRemove, Host.address (mod/host.py:4795) uses + # self.mapper.get_id(instance) -- the *creating* variant, which + # never raises -- so a never-seen instance safely resolves to + # pluginData is None and callback(False) fires synchronously. + body_json = json.dumps( + {"uri": "https://example.org/actuator", "minimum": 0, "maximum": 1, "value": 0.5} + ).encode("utf-8") + response, body = self.fetch_json( + "/effect/parameter/address/graph/nonexistent/gain", + method="POST", + body=body_json, + ) + self.assertEqual(response.code, 200) + self.assertIs(body, False) + + def test_address_missing_uri_is_404(self): + # EffectParameterAddress.post checks `data.get('uri', None) is + # None` before anything else and raises web.HTTPError(404). + response = self.fetch( + "/effect/parameter/address/graph/nonexistent/gain", + method="POST", + body=b"{}", + ) + self.assertEqual(response.code, 404) + + +class TestReset(ModUITestCase): + __test__ = True + + def test_reset_returns_true(self): + # SESSION.reset under FakeHMI (never initialized) takes the + # synchronous reset_host(True) branch straight to Host.reset, and + # FakeHost invokes every send_notmodified callback immediately with + # True. + response, body = self.fetch_json("/reset/") + self.assertEqual(response.code, 200) + self.assertIs(body, True) + + +class TestSetBufferSize(ModUITestCase): + __test__ = True + + def test_128_and_256_return_ok_false_size_zero(self): + # Matches the spec's own prediction: under MOD_DEV_ENVIRONMENT, + # IMAGE_VERSION is None so the /data/jack-buffer-size write is + # skipped, and set_jack_buffer_size (utils/utils_jack.cpp) is a + # null-guarded no-op that returns 0 without a JACK client -- so + # newsize (0) never equals the requested size, and 'ok' is always + # False. + for size in ("128", "256"): + response, body = self.fetch_json( + "/set_buffersize/%s" % size, method="POST", body=b"" + ) + self.assertEqual(response.code, 200) + self.assertEqual(body, {"ok": False, "size": 0}) + + +class TestResetXruns(ModUITestCase): + __test__ = True + + def test_reset_xruns_returns_true(self): + response, body = self.fetch_json("/reset_xruns/", method="POST", body=b"") + self.assertEqual(response.code, 200) + self.assertIs(body, True) + + +class TestJackMidiDevices(ModUITestCase): + __test__ = True + + def test_get_midi_devices_shape_under_fake_host(self): + # No JACK client -> Host.get_midi_ports() (backing + # web_get_midi_device_list) reports no devices; midiAggregatedMode + # defaults to True (mod/host.py:368, Host.__init__). + response, body = self.fetch_json("/jack/get_midi_devices") + self.assertEqual(response.code, 200) + self.assertEqual( + body, + { + "devsInUse": [], + "devList": [], + "names": {}, + "midiAggregatedMode": True, + }, + ) + + def test_set_midi_devices_matching_current_state_is_a_true_noop(self): + # devs=[], midiAggregatedMode=True, midiLoopback=False exactly match + # Host.__init__'s defaults (mod/host.py:368-369), so both the + # "mode changed" and "loopback changed" branches inside + # Host.set_midi_devices are skipped -- a side-effect-free call that + # still returns True (the handler always writes True on success). + body_json = json.dumps( + {"devs": [], "midiAggregatedMode": True, "midiLoopback": False} + ).encode("utf-8") + response, body = self.fetch_json( + "/jack/set_midi_devices", method="POST", body=body_json + ) + self.assertEqual(response.code, 200) + self.assertIs(body, True) + + def test_set_midi_devices_missing_key_is_500(self): + # Spec guessed "echo/true -- observe". Observed: JackSetMidiDevices + # .post does three unguarded dict subscripts (data['devs'], + # data['midiAggregatedMode'], data['midiLoopback']) with no + # try/except -- a body missing any of them raises an uncaught + # KeyError before any yield, which tornado turns into a plain 500, + # not a JSON error shape. + body_json = json.dumps({"devs": []}).encode("utf-8") + response = self.fetch( + "/jack/set_midi_devices", method="POST", body=body_json + ) + self.assertEqual(response.code, 500) + + +class TestTrueBypass(ModUITestCase): + __test__ = True + + def test_truebypass_returns_false_under_fake_jack(self): + # Matches the spec's own guess. set_truebypass_value (utils_jack) + # is null-guarded without a JACK client and its C-side setter + # reports failure, so the handler always writes False under the + # fakes, both channels, both requested states. + response, body = self.fetch_json("/truebypass/Left/true") + self.assertEqual(response.code, 200) + self.assertIs(body, False) + + response2, body2 = self.fetch_json("/truebypass/Right/false") + self.assertEqual(response2.code, 200) + self.assertIs(body2, False) + + +class TestTransportSetSyncMode(ModUITestCase): + __test__ = True + + def test_known_modes_return_true_then_restore_default(self): + # SESSION.reset() does NOT touch SESSION.host.profile, so this test + # explicitly restores the "/none" (internal/default) mode at the + # end to avoid leaking Profile state into later tests. + try: + for mode in ("/none", "/midi_clock_slave", "/link"): + response, body = self.fetch_json( + "/pedalboard/transport/set_sync_mode%s" % mode, + method="POST", + body=b"", + ) + self.assertEqual(response.code, 200) + self.assertIs(body, True) + finally: + self.fetch_json( + "/pedalboard/transport/set_sync_mode/none", method="POST", body=b"" + ) + + def test_invalid_mode_returns_false_not_error(self): + response, body = self.fetch_json( + "/pedalboard/transport/set_sync_mode/bogus", method="POST", body=b"" + ) + self.assertEqual(response.code, 200) + self.assertIs(body, False) + + +class TestCVAddressingPluginPortAdd(ModUITestCase): + __test__ = True + + def test_add_500s_without_an_existing_addressed_plugin_instance(self): + # Spec said "observe". Observed: PedalboardCvAddressingPluginPortAdd + # calls SESSION.web_cv_addressing_plugin_port_add synchronously + # (not yielded), which ends up in + # Host.addr_task_get_plugin_cv_port_op_mode (mod/host.py:970), + # which does self.mapper.get_id_without_creating(instance) on the + # instance embedded in the uri. Since no plugin instance can exist + # in this sandbox (POST /effect/add is banned -- see + # test/conftest.py NEVER-CALL LIST), this *always* raises KeyError + # and always 500s -- there is no well-formed uri that succeeds here + # without lilv. Unlike EffectRemove this is NOT a hang: the + # exception is raised synchronously (no @gen.coroutine boundary + # swallows it), so tornado's normal error path returns a fast 500. + body = urllib.parse.urlencode( + {"uri": "/cv/graph/nonexistent/cvout", "name": "test"} + ) + response = self.fetch( + "/pedalboard/cv_addressing_plugin_port/add", method="POST", body=body + ) + self.assertEqual(response.code, 500) From 9bce915266820deb09ba80ad3db5b89f3eca09f5 Mon Sep 17 00:00:00 2001 From: Luis Fagundes Date: Thu, 9 Jul 2026 14:34:49 +0000 Subject: [PATCH 06/14] Add WebSocket handshake characterization tests (phase 5) Pins the connect-time report_current_state burst pushed to every socket (sys_stats, stats, transport, truebypass, loading_start, size, then the loading_end ready marker), payload shapes for transport/truebypass/size, the identical burst on a second concurrent connection, and clean client close tolerance. Prefixes and arg counts only; volatile payloads unpinned. Co-Authored-By: Claude Fable 5 --- test/test_websocket.py | 284 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 test/test_websocket.py diff --git a/test/test_websocket.py b/test/test_websocket.py new file mode 100644 index 000000000..c7214d7f3 --- /dev/null +++ b/test/test_websocket.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for the /websocket connect-time handshake +(mod/webserver.py:ServerWebSocket, mod/session.py:Session.websocket_opened, +mod/host.py:Host.report_current_state -- all wired through +mod/development.py:FakeHost.open_connection_if_needed in this dev harness). + +Pins the message-PUSH sequence a client sees right after connecting: this is +the ``msg_callback`` broadcast path a future Tone3000 "file added" push +notification would ride. + +Exploratory findings (see docs/characterization-phase-5.md, verified against +this sandbox -- FakeHMI uninitialized, empty pedalboard, no CC devices, no +hardware ports, /proc/meminfo present so Host.memtimer is armed): + +- The connect-time push on the FIRST socket of the process is exactly 7 + ordered messages, by PREFIX (first whitespace-delimited word): + sys_stats, stats, transport, truebypass, loading_start, size, loading_end + The spec's sketch guessed a "stop"/ready-ish closing token; the real + ready-marker is ``loading_end `` + (mod/host.py:Host.report_current_state, last line before the per-plugin + addressing/HW sections, which are all empty in this sandbox). +- Immediately AFTER that 7-message burst, the first socket receives one + EXTRA ``sys_stats ...`` message. This is not part of + report_current_state's own push -- it is + FakeHost.open_connection_if_needed (mod/development.py) calling + ``self.memtimer_callback()`` synchronously, once, right after + ``report_current_state()`` returns, on the branch where + ``self.readsock``/``self.writesock`` were previously None (i.e. only for + the connection that actually "establishes" the fake host link). This is a + detail the spec's sketch did not anticipate; tests below treat it as an + unpinned trailing message (present in practice in this sandbox, but not + asserted on) rather than pin an exact total message count, per the phase's + "pin prefixes/arg-counts, not volatile detail" and "consume/ignore extra + stats messages" guidance. +- A SECOND, concurrent connection (opened while the first is still open) + gets the exact same 7-message report_current_state burst (every new + websocket gets its own full state dump -- Session.websocket_opened's + first-vs-not-first branch, session.py:236, only decides whether + Host.start_session runs first; it does not change what gets pushed to the + new socket). The observable difference is narrower than "first socket + session branch": it is FakeHost.open_connection_if_needed's *own* branch + on ``self.readsock is not None and self.writesock is not None`` -- true + for every connection after the first -- which skips + statstimer.start()/memtimer_callback() entirely. So only the very first + socket of the process gets the extra trailing sys_stats; the second socket + does not, even though both get an identical initial 7-message burst. +- PeriodicCallback (mod/host.py Host.statstimer/memtimer) binds + ``IOLoop.current()`` at construction time (tornado 4.3 + ioloop.py:PeriodicCallback.__init__), and SESSION.host is a process-level + singleton constructed at ``mod.webserver`` import time -- before any + per-test IOLoop exists. So ``statstimer.start()``/``memtimer.start()`` + schedule ticks on a *different*, never-pumped IOLoop; in practice no + additional "stats ..."/"sys_stats ..." messages arrive during a test's own + ~2s collection window. Tests still avoid asserting exact trailing counts, + since this is an artifact of import ordering, not a documented guarantee. +- Closing the client side of the connection is NOT synchronously reflected + server-side: right after ``conn.close()`` returns, GET /hello still + reports ``online: true`` (SESSION.websockets still has the entry) because + ``ServerWebSocket.on_close`` -> ``SESSION.websocket_closed`` only runs + once the server's IOLoop actually polls and processes the close frame. + This turned out FLAKY to observe deterministically: a bounded number of + ``gen.moment`` spins (pure callback-queue turns, no real I/O wait) is + sometimes enough and sometimes not. Per the phase spec's own guidance + ("count may update asynchronously; drop the assertion rather than + sleep-loop if flaky"), the close test below does not assert on the + post-close ``online`` value -- it only pins that the server tolerates the + close without erroring and keeps answering HTTP requests. +""" + +from tornado import gen, websocket +from tornado.testing import gen_test + +from base import ModUITestCase + +# Generous but bounded -- nothing here should ever legitimately take this +# long; a real hang (behavior regression) fails fast instead of hanging the +# whole suite. +_READ_TIMEOUT = 5.0 + +# Exact pinned prefix sequence of the connect-time report_current_state +# burst in this sandbox (empty pedalboard, no CC devices, no HW ports). +_EXPECTED_PREFIXES = [ + "sys_stats", + "stats", + "transport", + "truebypass", + "loading_start", + "size", + "loading_end", +] + +_READY_MARKER_PREFIX = "loading_end" + + +class WebSocketTestCase(ModUITestCase): + """Shared helpers. Not collected directly (__test__ stays False).""" + + __test__ = False + + def ws_url(self): + return self.get_url("/websocket").replace("http://", "ws://") + + @gen.coroutine + def read_one(self, conn, timeout=_READ_TIMEOUT): + """Read a single message with a hard timeout so a behavior change + (e.g. the ready marker disappearing) fails fast instead of hanging. + """ + msg = yield gen.with_timeout(self.io_loop.time() + timeout, conn.read_message()) + raise gen.Return(msg) + + @gen.coroutine + def drain_until_ready(self, conn, timeout=_READ_TIMEOUT): + """Collect messages up to and including the ready marker + (``loading_end ...``). Returns the list of messages, ready marker + included. Raises (via with_timeout / assertion) if the marker never + arrives or the socket closes first. + """ + messages = [] + while True: + msg = yield self.read_one(conn, timeout=timeout) + if msg is None: + self.fail( + "server closed the websocket before the ready marker " + "(%r) arrived; messages so far: %r" + % (_READY_MARKER_PREFIX, messages) + ) + messages.append(msg) + if msg.split(" ", 1)[0] == _READY_MARKER_PREFIX: + break + raise gen.Return(messages) + + +class TestWebSocketConnectSequence(WebSocketTestCase): + __test__ = True + + @gen_test(timeout=10) + def test_connect_pushes_pinned_prefix_sequence(self): + conn = yield websocket.websocket_connect(self.ws_url()) + try: + messages = yield self.drain_until_ready(conn) + prefixes = [m.split(" ", 1)[0] for m in messages] + self.assertEqual(prefixes, _EXPECTED_PREFIXES) + finally: + conn.close() + + @gen_test(timeout=10) + def test_transport_message_shape(self): + conn = yield websocket.websocket_connect(self.ws_url()) + try: + messages = yield self.drain_until_ready(conn) + transport_msgs = [m for m in messages if m.split(" ", 1)[0] == "transport"] + self.assertEqual(len(transport_msgs), 1) + + parts = transport_msgs[0].split(" ") + # "transport %i %f %f %s" -- prefix + 4 fields. + self.assertEqual(len(parts), 5) + rolling, bpb, bpm, sync = parts[1:] + + # arg-count + parseability is the load-bearing assertion; the + # concrete defaults below are deterministic Profile() config + # (not volatile like cpu%/timestamps), so pinning them is safe + # in this fresh-sandbox harness. + self.assertEqual(int(rolling), 0) + self.assertEqual(float(bpb), 4.0) + self.assertEqual(float(bpm), 120.0) + self.assertEqual(sync, "none") + finally: + conn.close() + + @gen_test(timeout=10) + def test_truebypass_message_reflects_defaults(self): + conn = yield websocket.websocket_connect(self.ws_url()) + try: + messages = yield self.drain_until_ready(conn) + tb_msgs = [m for m in messages if m.split(" ", 1)[0] == "truebypass"] + self.assertEqual(len(tb_msgs), 1) + + parts = tb_msgs[0].split(" ") + # "truebypass %i %i" -- prefix + 2 fields. + self.assertEqual(len(parts), 3) + left, right = int(parts[1]), int(parts[2]) + # Observed default (Host.last_true_bypass_left/right initial + # state in this dev sandbox): both channels report "true bypass + # on". + self.assertEqual((left, right), (1, 1)) + finally: + conn.close() + + @gen_test(timeout=10) + def test_size_message_present_with_two_numeric_args(self): + conn = yield websocket.websocket_connect(self.ws_url()) + try: + messages = yield self.drain_until_ready(conn) + size_msgs = [m for m in messages if m.split(" ", 1)[0] == "size"] + self.assertEqual(len(size_msgs), 1) + + parts = size_msgs[0].split(" ") + # "size %d %d" -- prefix + 2 fields. + self.assertEqual(len(parts), 3) + width, height = int(parts[1]), int(parts[2]) + # Empty/default pedalboard in a fresh sandbox -> zero size. + self.assertEqual((width, height), (0, 0)) + finally: + conn.close() + + @gen_test(timeout=10) + def test_second_concurrent_connection_gets_full_burst_but_no_trailing_extra(self): + conn1 = yield websocket.websocket_connect(self.ws_url()) + try: + messages1 = yield self.drain_until_ready(conn1) + prefixes1 = [m.split(" ", 1)[0] for m in messages1] + self.assertEqual(prefixes1, _EXPECTED_PREFIXES) + + # The first socket also gets one extra trailing sys_stats + # (FakeHost.open_connection_if_needed's memtimer_callback() + # kick, see module docstring). Consume it so it can't bleed + # into the second connection's read below; don't fail if it + # doesn't show up (it is an unpinned, environment-dependent + # detail). + try: + extra = yield self.read_one(conn1, timeout=1.0) + if extra is not None: + self.assertEqual(extra.split(" ", 1)[0], "sys_stats") + except gen.TimeoutError: + pass + + # Second connection, opened while the first is still open -- + # exercises Session.websocket_opened's "not the first socket" + # branch (session.py:236). + conn2 = yield websocket.websocket_connect(self.ws_url()) + try: + messages2 = yield self.drain_until_ready(conn2) + prefixes2 = [m.split(" ", 1)[0] for m in messages2] + # Same full report_current_state burst as the first socket. + self.assertEqual(prefixes2, _EXPECTED_PREFIXES) + + # But NOT the extra trailing sys_stats: FakeHost's + # readsock/writesock are already set by the time the second + # socket connects, so open_connection_if_needed takes the + # early-return branch (report_current_state only, no + # statstimer/memtimer kick). + with self.assertRaises(gen.TimeoutError): + yield self.read_one(conn2, timeout=1.0) + finally: + conn2.close() + finally: + conn1.close() + + +class TestWebSocketClose(WebSocketTestCase): + __test__ = True + + @gen_test(timeout=10) + def test_clean_close_no_server_error_and_hello_reflects_it(self): + conn = yield websocket.websocket_connect(self.ws_url()) + yield self.drain_until_ready(conn) + + response = yield self.http_client.fetch(self.get_url("/hello/")) + self.assertEqual(response.code, 200) + self.assertIn(b'"online": true', response.body) + + conn.close() + + # DROPPED (per phase-5 spec §6: "count may update asynchronously; + # drop the assertion rather than sleep-loop if flaky"): a strict + # assertion that /hello's `online` flips to false shortly after + # close. websocket_closed (session.py) only runs once the server's + # IOLoop actually polls and processes the close frame -- draining a + # bounded number of `gen.moment`s (pure callback-queue turns, no + # real I/O wait) was NOT enough to observe it in this harness + # (confirmed flaky: failed after 10 moments in a run where the + # exploratory script's 5-moments-after-two-fetches version happened + # to see it flip). A real wait would require an actual sleep/poll, + # which the spec explicitly says not to add. So this test only pins + # what's reliable: the server tolerates a client-initiated close + # without erroring, and a subsequent HTTP request still succeeds + # (well-formed JSON, 200) -- it does not assert on the `online` + # value after close. + response = yield self.http_client.fetch(self.get_url("/hello/")) + self.assertEqual(response.code, 200) + self.assertIn(b'"online"', response.body) From 37b07d5c43faf08b7503daabe048b1eb8787c5db Mon Sep 17 00:00:00 2001 From: Luis Fagundes Date: Thu, 9 Jul 2026 14:47:48 +0000 Subject: [PATCH 07/14] Add opt-in lilv plugin-endpoint characterization tests (phase 6) /effect/* endpoints need the global lilv world initialized or they segfault, so these tests are excluded from the default run via markers (lilv: empty-world behavior; lilv_fixture: a binaryless .lv2 fixture bundle). LV2_PATH points at an empty sandbox dir for determinism. Pins: /effect/list -> [], /effect/get on unknown uri -> 404 HTML, /effect/bulk skips unknown uris and 501s without a JSON content type, /effect/add on unknown uri mutates host state before failing late with 404. The two marker sets must never run in one process: a second modtools.utils.init() corrupts lilv's namespace singleton and segfaults at interpreter exit (documented in pytest.ini and both test modules). Co-Authored-By: Claude Fable 5 --- pytest.ini | 33 ++++ test/conftest.py | 21 +++ test/fixtures/phase6-fixture.lv2/manifest.ttl | 7 + .../phase6-fixture.lv2/phase6-fixture.ttl | 25 +++ test/test_effects_lilv.py | 174 ++++++++++++++++++ test/test_effects_lilv_fixture.py | 147 +++++++++++++++ 6 files changed, 407 insertions(+) create mode 100644 test/fixtures/phase6-fixture.lv2/manifest.ttl create mode 100644 test/fixtures/phase6-fixture.lv2/phase6-fixture.ttl create mode 100644 test/test_effects_lilv.py create mode 100644 test/test_effects_lilv_fixture.py diff --git a/pytest.ini b/pytest.ini index 3b84f2109..3655b1c20 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,36 @@ [pytest] testpaths = test python_files = test_*.py +markers = + lilv: tests that initialize the global lilv world (opt-in; run with -m lilv). + Deliberately excluded from the default run by addopts below -- see + docs/characterization-phase-6.md. These tests call modtools.utils.init(), + which is process-global and has no de-init in this suite (cleanup() + exists but is intentionally not called -- see test/test_effects_lilv.py). + A combined run (`pytest -m "(lilv or not lilv) and not lilv_fixture"` + -- see the lilv_fixture marker below for why the "and not lilv_fixture" + is required) was verified green with no observed leakage into other + tests as of phase 6, but -m lilv should still be treated as its own + invocation in CI to keep the blast radius of a lilv/JACK-adjacent + regression contained to one job. + lilv_fixture: the phase-6 stretch-goal tests (test/test_effects_lilv_fixture.py) + that scan a hand-written, binaryless .lv2 bundle. Opt-in; run with + -m lilv_fixture, and ONLY that -- as its OWN, separate pytest + invocation. DO NOT run together with -m lilv: modtools.utils.init() + is only safe to call ONCE per process. Calling it a second time (this + module's fixture vs. test_effects_lilv.py's) leaves lilv's + process-global NamespaceDefinitions singleton bound to a freed world + -- confirmed by direct reproduction to silently return empty + ports/category data and then SIGSEGV the interpreter on exit. Keeping + this marker's tests in a file of their own, with their own single + init() call and their own `pytest -m lilv_fixture` invocation, is not + a style preference, it is the only way to keep this suite + segfault-free. + DANGER, easy to get wrong: a bare `-m "lilv or not lilv"` (the + phase-6 spec's combined-run check) ALSO collects lilv_fixture tests, + because a lilv_fixture-only test is NOT marked lilv and therefore + satisfies "not lilv" -- that recreates the exact double-init crash + above. When running that combined check, always add + `and not lilv_fixture`: + `pytest -m "(lilv or not lilv) and not lilv_fixture"`. +addopts = -m "not lilv and not lilv_fixture" diff --git a/test/conftest.py b/test/conftest.py index d7f489118..a480d3325 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -90,11 +90,17 @@ _USER_FILES_DIR = os.path.join(_TEST_ROOT, "user-files") _PEDALBOARDS_DIR = os.path.join(_TEST_ROOT, "pedalboards") _PLUGINS_DIR = os.path.join(_TEST_ROOT, "lv2") +# Empty, dedicated -- deliberately NOT the same dir as _PLUGINS_DIR +# (MOD_USER_PLUGINS_DIR) above, to avoid conflating "where mod-ui thinks user +# plugins live" with "what lilv scans for the phase-6 tests" even though +# both happen to be empty today. +_LV2_SCAN_DIR = os.path.join(_TEST_ROOT, "lv2-path-empty") os.makedirs(_DATA_DIR, exist_ok=True) os.makedirs(_USER_FILES_DIR, exist_ok=True) os.makedirs(_PEDALBOARDS_DIR, exist_ok=True) os.makedirs(_PLUGINS_DIR, exist_ok=True) +os.makedirs(_LV2_SCAN_DIR, exist_ok=True) # Replicate the bits of mod.check_environment() that handlers rely on but # that we never call (check_environment() only runs inside prepare()). @@ -115,6 +121,21 @@ os.environ["MOD_USER_PEDALBOARDS_DIR"] = _PEDALBOARDS_DIR os.environ["MOD_USER_PLUGINS_DIR"] = _PLUGINS_DIR +# Phase 6 (docs/characterization-phase-6.md): lilv (the C++ library backing +# modtools.utils.init()) reads the LV2_PATH env var itself, via plain getenv, +# at lilv_world_load_all() time inside init() -- not at Python import time +# and not cached anywhere in Python. Confirmed by reading utils/utils_lilv.cpp +# init() (~:3893): it only calls lilv_world_new()/lilv_world_load_all(W), with +# no explicit LV2_PATH manipulation of its own (unlike e.g. get_all_pedalboards +# a few hundred lines below, which saves/restores LV2_PATH around its own +# private world). So setting os.environ["LV2_PATH"] here, well before any +# phase-6 test module even calls init() at fixture time, is sufficient -- +# CPython's os.environ.__setitem__ calls os.putenv() under the hood, which is +# what a later getenv() in the C library will see. Point it at an empty, +# sandboxed dir so the lilv world phase 6 builds is always empty and +# deterministic, never the machine's real ~/.lv2 or /usr/lib/lv2. +os.environ["LV2_PATH"] = _LV2_SCAN_DIR + # ---------------------------------------------------------------------------- # 1b. Sandbox guard: every writable path mod.settings resolves must live # under the throwaway test root. mod.settings only reads env vars and diff --git a/test/fixtures/phase6-fixture.lv2/manifest.ttl b/test/fixtures/phase6-fixture.lv2/manifest.ttl new file mode 100644 index 000000000..636a5adb3 --- /dev/null +++ b/test/fixtures/phase6-fixture.lv2/manifest.ttl @@ -0,0 +1,7 @@ +@prefix lv2: . +@prefix rdfs: . + + + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . diff --git a/test/fixtures/phase6-fixture.lv2/phase6-fixture.ttl b/test/fixtures/phase6-fixture.lv2/phase6-fixture.ttl new file mode 100644 index 000000000..8c8561d25 --- /dev/null +++ b/test/fixtures/phase6-fixture.lv2/phase6-fixture.ttl @@ -0,0 +1,25 @@ +@prefix lv2: . +@prefix doap: . +@prefix rdfs: . +@prefix foaf: . + + + a lv2:Plugin, lv2:UtilityPlugin ; + doap:name "Phase 6 Fixture" ; + doap:license ; + doap:maintainer [ + foaf:name "mod-ui characterization tests" + ] ; + lv2:microVersion 0 ; + lv2:minorVersion 1 ; + lv2:port [ + a lv2:InputPort, lv2:AudioPort ; + lv2:index 0 ; + lv2:symbol "in" ; + lv2:name "In" + ] , [ + a lv2:OutputPort, lv2:AudioPort ; + lv2:index 1 ; + lv2:symbol "out" ; + lv2:name "Out" + ] . diff --git a/test/test_effects_lilv.py b/test/test_effects_lilv.py new file mode 100644 index 000000000..6262d40c0 --- /dev/null +++ b/test/test_effects_lilv.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Characterization tests for the /effect/* plugin-info handlers +(mod/webserver.py: EffectList, EffectGet, EffectGetNonCached, EffectBulk, +EffectAdd) that dereference the global lilv world (utils/utils_lilv.cpp +W/PLUGINS, initialized by modtools.utils.init()). + +Phase 6 (docs/characterization-phase-6.md): this whole module is the +controlled, opt-in exception to the test/conftest.py NEVER-CALL LIST entry +for these exact routes. Every test here runs against an EMPTY lilv world +(LV2_PATH pointed at a dedicated empty sandbox dir in test/conftest.py) -- +no real plugin is ever scanned, so every URI used below is, from lilv's +point of view, unknown/nonexistent. + +Opt-in via the `lilv` pytest marker (see pytest.ini): `pytestmark` below +marks every test in this module, and pytest.ini's `addopts = -m "not lilv"` +excludes the whole module from the default `pytest` run. Run explicitly with +`pytest -m lilv`. + +modtools.utils.init() is the exact function mod/webserver.py's lv2_init() +(aliased import at webserver.py:52, called inside prepare() at :2451) calls +-- confirmed by reading modtools/utils.py's init() (:708), which is a bare +`utils.init()` ctypes call into utils/utils_lilv.cpp init() (:3893: +lilv_world_free + lilv_world_new + lilv_world_load_all + namespace setup, +no JACK calls). This fixture calls the same modtools.utils.init(), skipping +the rest of webserver.prepare() (HMI/host/JACK setup), matching the spec's +guidance to call the same underlying function without the surrounding +production bootstrap. + +No de-init: modtools.utils.cleanup() exists but is deliberately not called +here -- it frees the global lilv world (W = nullptr), and any other test +process code that runs after it (including non-lilv tests, if ever run in +the same process) would then be back to square one, NULL-W, for any /effect/* +call. Leaving the world initialized-but-empty for the rest of the process +is the safer of two bad options and is exactly why this suite keeps these +tests opt-in/marker-gated rather than trying to isolate them automatically. + +The stretch-goal hand-written-.lv2-bundle tests (docs/characterization- +phase-6.md) live in a SEPARATE file, test/test_effects_lilv_fixture.py, +under a SEPARATE marker (`lilv_fixture`), run as its own invocation +(`pytest -m lilv_fixture`) -- NEVER combined in the same process as this +module's `lilv_world` fixture. Reason (load-bearing finding, see that +file's docstring): modtools.utils.init() is safe to call once per process, +but calling it a SECOND time (e.g. to reload with a bundle present) does +not correctly reset lilv's NamespaceDefinitions::getStaticInstance() +singleton (utils/utils_lilv.cpp ~:540), which is a process-global object +independent of the LilvWorld it was last init()ed with. A second init() +call left get_plugin_info()'s ports/category silently empty (instead of +the real data) and reliably crashed the interpreter on process exit +(SIGSEGV during native static/global teardown) -- confirmed by direct +reproduction, not a guess. One init() per process is a hard constraint of +this C extension as used here. +""" + +import json + +from base import ModUITestCase + +import pytest + +pytestmark = pytest.mark.lilv + + +@pytest.fixture(scope="module", autouse=True) +def lilv_world(): + from modtools import utils + utils.init() + yield + # Deliberately no cleanup()/de-init -- see module docstring. + + +class TestEffectList(ModUITestCase): + __test__ = True + + def test_empty_world_returns_empty_list(self): + response = self.fetch("/effect/list") + self.assertEqual(response.code, 200) + content_type = response.headers.get("Content-Type", "") + self.assertIn("application/json", content_type) + self.assertEqual(json.loads(response.body.decode("utf-8")), []) + + +class TestEffectGet(ModUITestCase): + __test__ = True + + def test_unknown_uri_is_404(self): + # get_plugin_info() (modtools/utils.py) raises when the C lookup + # returns NULL for an unknown URI; EffectGet's bare `except:` around + # it turns that into web.HTTPError(404). CachedJsonRequestHandler + # does not override write_error, so this is tornado's default HTML + # error page, NOT a JSON body -- unlike every other handler in this + # suite, do not decode this response as JSON. + response = self.fetch("/effect/get?uri=urn:nonexistent") + self.assertEqual(response.code, 404) + + +class TestEffectGetNonCached(ModUITestCase): + __test__ = True + + def test_unknown_uri_is_404(self): + # Same shape as EffectGet: get_non_cached_plugin_info() raises for + # an unknown URI, caught by the handler's bare `except:`, re-raised + # as web.HTTPError(404) -> tornado's default HTML error page. + response = self.fetch("/effect/get_non_cached?uri=urn:nonexistent") + self.assertEqual(response.code, 404) + + +class TestEffectBulk(ModUITestCase): + __test__ = True + + def _post_bulk(self, uris): + return self.fetch( + "/effect/bulk/", + method="POST", + headers={"Content-Type": "application/json"}, + body=json.dumps(uris), + ) + + def test_empty_list_returns_empty_object(self): + response = self._post_bulk([]) + self.assertEqual(response.code, 200) + content_type = response.headers.get("Content-Type", "") + self.assertIn("application/json", content_type) + self.assertEqual(json.loads(response.body.decode("utf-8")), {}) + + def test_unknown_uris_are_silently_skipped(self): + # EffectBulk.post() calls get_plugin_info(uri) per URI inside a bare + # try/except that `continue`s on failure -- an unknown URI simply + # never makes it into the result dict, no error surfaced at all. + response = self._post_bulk(["urn:nonexistent-1", "urn:nonexistent-2"]) + self.assertEqual(response.code, 200) + self.assertEqual(json.loads(response.body.decode("utf-8")), {}) + + def test_missing_content_type_is_501(self): + # EffectBulk.prepare() requires "application/json" in Content-Type, + # else raises web.HTTPError(501) before post() ever runs. + response = self.fetch("/effect/bulk/", method="POST", body=json.dumps([])) + self.assertEqual(response.code, 501) + + +class TestEffectAdd(ModUITestCase): + __test__ = True + + def test_unknown_uri_ends_in_404_not_false(self): + # SPEC DEVIATION (docs/characterization-phase-6.md guessed "false?"): + # observed behavior is HTTP 404, not a JSON `false` body. Traced: + # 1. EffectAdd.get() does `ok = yield gen.Task(SESSION.web_add, ...)`. + # 2. web_add -> Host.add_plugin -> FakeHost.send_modified(msg, host_callback) + # (mod/development.py) calls host_callback(True) unconditionally + # and synchronously -- no real mod-host round trip. + # 3. Inside add_plugin's host_callback, `resp` is `True`; the guard + # is `if resp < 0`, and `True < 0` is False in Python, so it does + # NOT bail out early despite the URI being unknown. + # 4. It then calls get_plugin_info_essentials(uri) (modtools/utils.py), + # which is NULL-safe: an unknown URI returns a defaults dict + # (`{'error': True, 'controlInputs': [], ...}`) instead of + # raising. So add_plugin finishes normally, registers a fake + # plugin instance in SESSION.host.plugins, and calls + # callback(True) -- i.e. `ok` is True back in EffectAdd.get(). + # 5. Because ok is truthy, EffectAdd.get() proceeds to + # `data = get_plugin_info(uri)` (NOT get_plugin_info_essentials) + # -- this one DOES raise for an unknown URI (see TestEffectGet + # above), caught by EffectAdd's own bare `except:`, which raises + # web.HTTPError(404). + # Net effect: an unknown-URI add is NOT rejected up front; it silently + # registers bookkeeping state on SESSION.host before failing late, + # with a 404 (tornado's default HTML error page) as the only visible + # signal to the caller -- no JSON, no `false`. + response = self.fetch( + "/effect/add/lilv_phase6_unknown?uri=urn:nonexistent&x=0&y=0" + ) + self.assertEqual(response.code, 404) diff --git a/test/test_effects_lilv_fixture.py b/test/test_effects_lilv_fixture.py new file mode 100644 index 000000000..5bb357d75 --- /dev/null +++ b/test/test_effects_lilv_fixture.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Phase 6 (docs/characterization-phase-6.md) STRETCH GOAL: a minimal +hand-written .lv2 bundle (test/fixtures/phase6-fixture.lv2/ -- manifest.ttl ++ phase6-fixture.ttl, deliberately NO binary/.so file), scanned into its own +lilv world, to pin one real /effect/list entry shape and one real +/effect/get field set -- as opposed to test_effects_lilv.py, which only +exercises an empty world / unknown URIs. + +Confirmed: lilv does NOT need the plugin binary to exist on disk for +metadata listing or get_plugin_info(). lilv_world_load_all() only RDF-parses +manifest.ttl and the rdfs:seeAlso'd turtle file; it never dlopen()s +lv2:binary at scan time (only real audio instantiation would). manifest.ttl +here points lv2:binary at "phase6-fixture.so", a file that is never created +anywhere -- the scan and both endpoints below work anyway. So the spec's +"if lilv refuses a binaryless bundle, drop the stretch" escape hatch was not +needed for the *lilv* half of this stretch goal. + +THIS FILE MUST BE RUN AS ITS OWN INVOCATION: `pytest -m lilv_fixture`, never +combined with `-m lilv` (test_effects_lilv.py) in the same process, and +never via a marker expression that would pull both in (e.g. do NOT run +`pytest -m "lilv or lilv_fixture"`). This is a hard constraint, not +caution for its own sake -- it is the actual reason the stretch goal ended +up in its own file instead of a second test class inside +test_effects_lilv.py (which is where it lived during development). + +What went wrong when it *was* a second class in the same file/process, +reproduced directly: that class's setUpClass copied the fixture bundle into +a fresh directory, repointed LV2_PATH, and called modtools.utils.init() a +SECOND time in the process (the module-scoped `lilv_world` fixture in +test_effects_lilv.py having already called it once for the empty world). +modtools/utils.py init() is a bare ctypes call into utils/utils_lilv.cpp +init() (:3893), which does free the previous LilvWorld and build a new one +-- but it also re-runs `NamespaceDefinitions::getStaticInstance(W).init(W)` +(:3898). getStaticInstance() (~:540) is a process-wide C++ singleton +(function-local `static`), NOT reset or freed between the two init() calls +(cleanup() -- the only code path that calls +`NamespaceDefinitions::...cleanup()` -- is, per the phase-6 spec, never +called in this suite). The result, reproduced directly: the second world's +plugin was found and get_all_plugins()/get_plugin_info() returned it, but +its `ports` and `category` fields were silently empty (`[]`) instead of the +real port/category data -- no exception, no crash at that point, just wrong +data, because the namespace lookups the C++ code uses to classify ports/ +categories were resolved against the FIRST (already-freed) world's nodes. +Worse, the process then reliably SIGSEGV'd during native static/global +destructor teardown at interpreter exit (after pytest had already printed +its full PASSED/FAILED summary -- so a naive glance at "tests passed" would +have missed it; `timeout ... ; echo $?` showed "dumped core"). This is +exactly the DANGER-level failure mode the phase-6 spec's NEVER-CALL-LIST +reasoning warns about, just triggered by a second init() instead of a +pre-init call. + +The fix applied: this module now calls modtools.utils.init() exactly ONCE, +as the ONLY init() call in its own process, matching the one-call invariant +that makes test_effects_lilv.py itself safe. +""" + +import json +import os +import shutil + +from base import ModUITestCase + +import pytest + +pytestmark = pytest.mark.lilv_fixture + +_FIXTURE_BUNDLE_SRC = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "fixtures", "phase6-fixture.lv2" +) +_FIXTURE_PLUGIN_URI = "http://example.org/plugins/phase6-fixture" + + +@pytest.fixture(scope="module", autouse=True) +def lilv_world_with_fixture_bundle(): + from conftest import _TEST_ROOT + from modtools import utils + + scan_dir = os.path.join(_TEST_ROOT, "lv2-path-fixture-bundle") + if os.path.isdir(scan_dir): + shutil.rmtree(scan_dir) + os.makedirs(scan_dir) + shutil.copytree(_FIXTURE_BUNDLE_SRC, os.path.join(scan_dir, "phase6-fixture.lv2")) + + # See module docstring: os.environ["LV2_PATH"] is read by lilv itself, + # inside init(), at call time (test/conftest.py sets an initial empty-dir + # value for test_effects_lilv.py; here we override it before this + # module's ONE AND ONLY init() call in this process). + os.environ["LV2_PATH"] = scan_dir + utils.init() + yield + # No cleanup()/de-init -- same reasoning as test_effects_lilv.py, and + # moot here since this is the last thing this process ever does. + + +class TestEffectListWithFixtureBundle(ModUITestCase): + __test__ = True + + def test_effect_list_contains_fixture_plugin_mini_shape(self): + response = self.fetch("/effect/list") + self.assertEqual(response.code, 200) + plugins = json.loads(response.body.decode("utf-8")) + + by_uri = {p["uri"]: p for p in plugins} + self.assertIn(_FIXTURE_PLUGIN_URI, by_uri) + + entry = by_uri[_FIXTURE_PLUGIN_URI] + # Pin the PluginInfo_Mini field set (get_all_plugins), not the full + # PluginInfo shape (that's EffectGet, checked separately below). + self.assertEqual(entry["name"], "Phase 6 Fixture") + self.assertEqual(entry["label"], "Phase 6 Fixture") + self.assertEqual(entry["category"], ["Utility"]) + self.assertEqual( + set(entry.keys()), + { + "uri", "name", "brand", "label", "comment", "buildEnvironment", + "category", "microVersion", "minorVersion", "release", + "builder", "licensed", "iotype", "gui", + }, + ) + + +class TestEffectGetWithFixtureBundle(ModUITestCase): + __test__ = True + + def test_effect_get_returns_full_info_for_fixture_plugin(self): + response = self.fetch("/effect/get?uri=" + _FIXTURE_PLUGIN_URI) + self.assertEqual(response.code, 200) + content_type = response.headers.get("Content-Type", "") + self.assertIn("application/json", content_type) + + data = json.loads(response.body.decode("utf-8")) + self.assertTrue(data["valid"]) + self.assertEqual(data["uri"], _FIXTURE_PLUGIN_URI) + self.assertEqual(data["name"], "Phase 6 Fixture") + # lv2:binary in manifest.ttl points at a .so that was never created + # on disk -- confirms the binary is not required for full plugin + # info (only real audio instantiation would need it). + self.assertEqual(data["binary"], "") + self.assertEqual( + [p["symbol"] for p in data["ports"]["audio"]["input"]], ["in"] + ) + self.assertEqual( + [p["symbol"] for p in data["ports"]["audio"]["output"]], ["out"] + ) From 959250d2434e2e3a46e70a795561a5dd8ddc5566 Mon Sep 17 00:00:00 2001 From: Luis Fagundes Date: Fri, 10 Jul 2026 19:49:21 +0000 Subject: [PATCH 08/14] webserver: add a POST /files/upload route for writing user files mod-ui lists the user-files tree (GET /files/list) but has never written to it: uploads belong to the separate file-manager service on port 8081, which is a human-facing iframe a page cannot drive programmatically. The upcoming Tone3000 integration downloads model files in the browser and needs exactly one server-side capability -- writing the received bytes to the device's disk -- so add the narrowest route that does that. POST /files/upload/?folder=X&name=Y with an application/octet-stream body writes USER_FILES_DIR//X/Y. The folder/extension policy is reused from FilesList._get_dir_and_extensions_for_filetype, so only filetypes that map to a real category folder are accepted, with their extension enforced. folder and name must each be a single path component -- anything os.path.basename would rewrite is refused rather than silently rewritten -- and the content-type gate keeps every cross-origin POST behind a CORS preflight that mod-ui never answers. The response carries the same fullname string /files/list builds by walking, so a caller can find its own upload in the list. Co-Authored-By: Claude Fable 5 --- mod/webserver.py | 33 ++++++++++++++ test/test_files_upload.py | 95 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 test/test_files_upload.py diff --git a/mod/webserver.py b/mod/webserver.py index 808c8532b..fe1971f74 100644 --- a/mod/webserver.py +++ b/mod/webserver.py @@ -2260,6 +2260,38 @@ def get(self): 'files': tuple(retfiles[fn] for fn in fullnames), }) +class FilesUpload(JsonRequestHandler): + def post(self, filetype): + datadir, extensions = FilesList._get_dir_and_extensions_for_filetype(filetype) + if datadir is None: + raise web.HTTPError(400) + + # Anything outside the 3 CORS "simple" content-types forces a preflight we do not answer, + # so a cross-origin page cannot reach this handler. + if self.request.headers.get("Content-Type") != "application/octet-stream": + raise web.HTTPError(400) + + # basename() alone would quietly turn "../../etc" into "etc" and write it anyway. + # Refuse anything it would rewrite, so a caller never gets a file somewhere it did + # not ask for. Both are single path components by the time we join them. + folder = self.get_argument("folder", "") + name = self.get_argument("name", "") + if folder != os.path.basename(folder) or name != os.path.basename(name): + raise web.HTTPError(400) + if folder in ("", ".", "..") or name in ("", ".", ".."): + raise web.HTTPError(400) + if not name.lower().endswith(extensions): + raise web.HTTPError(400) + + destdir = os.path.join(USER_FILES_DIR, datadir, folder) + os.makedirs(destdir, exist_ok=True) + fullname = os.path.join(destdir, name) + with open(fullname, 'wb') as fh: + fh.write(self.request.body) + + # Same string FilesList builds by walking, so a caller can match on it. + self.write({'ok': True, 'fullname': fullname}) + settings = {'log_function': lambda handler: None} if not LOG else {} application = web.Application( @@ -2352,6 +2384,7 @@ def get(self): # file listing etc (r"/files/list/?", FilesList), + (r"/files/upload/([a-z]+)/?", FilesUpload), (r"/reset/?", DashboardClean), diff --git a/test/test_files_upload.py b/test/test_files_upload.py new file mode 100644 index 000000000..89669cafd --- /dev/null +++ b/test/test_files_upload.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +# SPDX-License-Identifier: AGPL-3.0-or-later + +"""Tests for POST /files/upload/ (mod/webserver.py:FilesUpload). + +The route is the one server-side piece of the Tone3000 download: a browser +page fetches the model bytes itself and hands them here to be written under +USER_FILES_DIR. Everything it refuses, it refuses with a plain 400 *before* +touching the disk -- these tests pin both the happy path and each refusal. +""" + +import json +import os +from urllib.parse import urlencode + +from base import ModUITestCase +from conftest import _USER_FILES_DIR + + +def upload_url(filetype="nammodel", folder="My Tone (12)", name="model.nam"): + return "/files/upload/%s?%s" % (filetype, urlencode({"folder": folder, "name": name})) + + +class TestFilesUpload(ModUITestCase): + __test__ = True + + def upload(self, body=b"nam-bytes", content_type="application/octet-stream", **kwargs): + headers = {"Content-Type": content_type} if content_type is not None else None + return self.fetch(upload_url(**kwargs), method="POST", body=body, headers=headers) + + def user_files(self): + found = [] + for root, _, files in os.walk(str(_USER_FILES_DIR)): + found.extend(os.path.join(root, f) for f in files) + return found + + def test_happy_path_writes_and_reports_the_fullname_files_list_shows(self): + response = self.upload(folder="Clean Amp (7)", name="Clean Amp - Standard.nam") + self.assertEqual(response.code, 200) + + body = json.loads(response.body.decode("utf-8")) + self.assertTrue(body["ok"]) + self.assertTrue(body["fullname"].endswith("NAM Models/Clean Amp (7)/Clean Amp - Standard.nam")) + + with open(body["fullname"], "rb") as fh: + self.assertEqual(fh.read(), b"nam-bytes") + + # The reported fullname is byte-for-byte what /files/list will show, + # so a caller can find its own upload in the list. + _, listing = self.fetch_json("/files/list?types=nammodel") + self.assertEqual([f["fullname"] for f in listing["files"]], [body["fullname"]]) + + def test_extension_check_is_case_insensitive_like_files_list(self): + response = self.upload(name="AMP.NAM") + self.assertEqual(response.code, 200) + + def test_unknown_filetype_is_400(self): + self.assertEqual(self.upload(filetype="bogus").code, 400) + + def test_unmapped_filetype_is_400(self): + # "nam" is a real mod:fileTypes value the NAM plugin declares, but the + # category map deliberately returns no folder for it -- only "nammodel" + # (and "aidadspmodel") map to a directory. + self.assertEqual(self.upload(filetype="nam").code, 400) + + def test_wrong_content_type_is_400(self): + # Anything except application/octet-stream is refused; among other + # things this keeps every cross-origin POST behind a CORS preflight + # that mod-ui never answers. + self.assertEqual(self.upload(content_type="text/plain").code, 400) + self.assertEqual(self.upload(content_type="multipart/form-data").code, 400) + + def test_wrong_extension_is_400(self): + self.assertEqual(self.upload(name="model.wav").code, 400) + self.assertEqual(self.upload(name="model").code, 400) + + def test_traversal_in_name_or_folder_is_400_and_writes_nothing(self): + for folder, name in ( + ("..", "model.nam"), # parent dir as the folder + ("a/b", "model.nam"), # separator smuggled into folder + ("pack", "../evil.nam"), # separator smuggled into name + ): + response = self.upload(folder=folder, name=name) + self.assertEqual(response.code, 400, "folder=%r name=%r" % (folder, name)) + + self.assertEqual(self.user_files(), [], "a refused upload must write nothing") + + def test_empty_or_dot_components_are_400(self): + for folder, name in (("", "model.nam"), (".", "model.nam"), + ("pack", ""), ("pack", "."), ("pack", "..")): + response = self.upload(folder=folder, name=name) + self.assertEqual(response.code, 400, "folder=%r name=%r" % (folder, name)) + + self.assertEqual(self.user_files(), []) From 45850312b9bb4f7e908ff892e5097d3da1a48509 Mon Sep 17 00:00:00 2001 From: Luis Fagundes Date: Fri, 10 Jul 2026 19:53:25 +0000 Subject: [PATCH 09/14] Add a Tone3000 tab to the main menu TONE3000 (tone3000.com) hosts NAM amp-capture models -- the files the Neural Amp Modeler plugin loads from the NAM Models user-files folder. This adds the entry point for browsing it from the device UI: a new icon in the bottom-left #main-menu cluster and its full-screen overlay panel, cloned from the File Manager precedent (trigger icon + #*-library panel + JqueryClass box + Desktop wiring + statusTooltip). The panel itself only introduces the tab and explains what it is for; the way into the catalog lands next. Co-Authored-By: Claude Fable 5 --- html/css/less/main.less | 17 ++++++++++++- html/css/less/tone3000.less | 49 ++++++++++++++++++++++++++++++++++++ html/css/main.css | 15 ++++++++++- html/img/tone3000-icon.png | Bin 0 -> 3445 bytes html/index.html | 21 ++++++++++++++++ html/js/desktop.js | 13 ++++++++++ html/js/tone3000.js | 16 ++++++++++++ 7 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 html/css/less/tone3000.less create mode 100644 html/img/tone3000-icon.png create mode 100644 html/js/tone3000.js diff --git a/html/css/less/main.less b/html/css/less/main.less index 2e0eed41c..5afd1a9f5 100644 --- a/html/css/less/main.less +++ b/html/css/less/main.less @@ -72,6 +72,7 @@ @import 'cloud_plugins.less'; @import 'banks.less'; @import 'file_manager.less'; +@import 'tone3000.less'; @import 'pedalboards.less'; @import 'nprogress.less'; @import 'addressings.less'; @@ -350,6 +351,19 @@ hr.dotted { background-position: -180px bottom; } } + #mod-tone3000 { + background-image: url(../img/tone3000-icon.png); + background-position: center center; + background-size: 30px 30px; + background-repeat: no-repeat; + transition: all 0.33s; + &:hover { + background-color: #000; + } + &.selected { + background-color: #000; + } + } #banks-saving { margin: 21px 0 0 6px; font-size: 10px; @@ -517,7 +531,8 @@ header#pedalboard-actions, #bank-library header, #cloud-plugins-library header, #pedalboards-library header, -#file-manager-library header { +#file-manager-library header, +#tone3000-library header { display: block; left: 0; height: 45px; diff --git a/html/css/less/tone3000.less b/html/css/less/tone3000.less new file mode 100644 index 000000000..f4fedf0f7 --- /dev/null +++ b/html/css/less/tone3000.less @@ -0,0 +1,49 @@ +#tone3000-library { + background: #2c2c2c url(../img/watermark.png) 100% 100% no-repeat; + top: 0; + bottom: 46px; + left: 0; + right: 0; + overflow-x: hidden; + overflow-y: auto; + position: absolute; + z-index: 2; +} +#tone3000-wrapper { + position: absolute; + top: 45px; + bottom: 46px; + left: 0; + right: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + color: #ccc; +} +#tone3000-logo { + width: 96px; + height: 96px; +} +#tone3000-wrapper h2 { + color: #fff; + font-size: 24px; + font-weight: normal; + text-transform: uppercase; + margin: .8em 0 .2em; +} +#tone3000-wrapper .tone3000-hint { + font-size: 13px; + line-height: 1.5; + color: #999; + max-width: 34em; + margin: 0 0 1em; +} +#tone3000-wrapper .tone3000-hint b { + color: #ccc; + font-weight: normal; +} +#tone3000-wrapper .tone3000-hint:last-of-type { + margin-bottom: 1.5em; +} diff --git a/html/css/main.css b/html/css/main.css index 9b948034e..c8ffd5fd0 100644 --- a/html/css/main.css +++ b/html/css/main.css @@ -1 +1,14 @@ -/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,*:before,*:after{color:#000 !important;text-shadow:none !important;background:transparent !important;box-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#883996;text-decoration:none}a:hover,a:focus{color:#56245f;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:0}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:0;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#999}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{padding:.2em;background-color:#f29446}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#999}.text-primary{color:#883996}a.text-primary:hover,a.text-primary:focus{color:#662b71}.text-success{color:#3c763d}a.text-success:hover,a.text-success:focus{color:#2b542c}.text-info{color:#3a633d}a.text-info:hover,a.text-info:focus{color:#274329}.text-warning{color:#fff}a.text-warning:hover,a.text-warning:focus{color:#e6e6e6}.text-danger{color:#a94442}a.text-danger:hover,a.text-danger:focus{color:#843534}.bg-primary{color:#fff;background-color:#883996}a.bg-primary:hover,a.bg-primary:focus{background-color:#662b71}.bg-success{background-color:#dff0d8}a.bg-success:hover,a.bg-success:focus{background-color:#c1e2b3}.bg-info{background-color:#a6ecb1}a.bg-info:hover,a.bg-info:focus{background-color:#7ce38c}.bg-warning{background-color:#f29446}a.bg-warning:hover,a.bg-warning:focus{background-color:#ef7816}.bg-danger{background-color:#f2dede}a.bg-danger:hover,a.bg-danger:focus{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#999}blockquote footer:before,blockquote small:before,blockquote .small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:""}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:"\00A0 \2014"}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:0}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:0;box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:0}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:100%}}@media (min-width:992px){.container{width:970px}}@media (min-width:1240px){.container{width:1270px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*="col-"]{padding-right:0;padding-left:0}.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1240px){.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}table col[class*="col-"]{position:static;display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#999;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#a6ecb1}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#91e89f}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#f29446}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#f0862e}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none;appearance:none}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);box-shadow:inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{line-height:34px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:30px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:46px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-top:4px \9;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:0}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:0}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm textarea.form-control,.form-group-sm select[multiple].form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:0}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:0}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg textarea.form-control,.form-group-lg select[multiple].form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.33}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback,.input-group-lg+.form-control-feedback,.form-group-lg .form-control+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback,.input-group-sm+.form-control-feedback,.form-group-sm .form-control+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#fff}.has-warning .form-control{border-color:#fff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-warning .input-group-addon{color:#fff;background-color:#f29446;border-color:#fff}.has-warning .form-control-feedback{color:#fff}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;white-space:nowrap;vertical-align:middle;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);opacity:.65;-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:focus,.btn-default.focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#883996;border-color:#773284}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#662b71;border-color:#230f27}.btn-primary:hover{color:#fff;background-color:#662b71;border-color:#4f2157}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#662b71;background-image:none;border-color:#4f2157}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#4f2157;border-color:#230f27}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#883996;border-color:#773284}.btn-primary .badge{color:#883996;background-color:#fff}.btn-success{color:#fff;background-color:#883996;border-color:#773284}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#662b71;border-color:#230f27}.btn-success:hover{color:#fff;background-color:#662b71;border-color:#4f2157}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#662b71;background-image:none;border-color:#4f2157}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#fff;background-color:#4f2157;border-color:#230f27}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#883996;border-color:#773284}.btn-success .badge{color:#883996;background-color:#fff}.btn-info{color:#fff;background-color:#883996;border-color:#773284}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#662b71;border-color:#230f27}.btn-info:hover{color:#fff;background-color:#662b71;border-color:#4f2157}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#662b71;background-image:none;border-color:#4f2157}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#fff;background-color:#4f2157;border-color:#230f27}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#883996;border-color:#773284}.btn-info .badge{color:#883996;background-color:#fff}.btn-warning{color:#fff;background-color:#f29446;border-color:#f0862e}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#ef7816;border-color:#95490a}.btn-warning:hover{color:#fff;background-color:#ef7816;border-color:#d3680f}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ef7816;background-image:none;border-color:#d3680f}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#fff;background-color:#d3680f;border-color:#95490a}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#f29446;border-color:#f0862e}.btn-warning .badge{color:#f29446;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;background-image:none;border-color:#ac2925}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#883996;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#56245f;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:0}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:0}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:0}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height, visibility;transition-property:height, visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid \9;border-right:4px solid transparent;border-left:4px solid transparent}.dropup,.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:0;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#883996;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#999;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid \9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle="buttons"]>.btn input[type="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:0}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:0}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:0}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:0}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:0}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#883996}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:0 0 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:0 0 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:0}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#883996}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:0 0 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:0}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:0}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-right:-15px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left !important}.navbar-right{float:right !important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#999}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:0}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:0}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#883996;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{z-index:2;color:#56245f;background-color:#eee;border-color:#ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:3;color:#fff;cursor:default;background-color:#883996;border-color:#883996}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.33}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:0;border-bottom-left-radius:0}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:0;border-bottom-left-radius:0}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#883996}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#662b71}.label-success{background-color:#883996}.label-success[href]:hover,.label-success[href]:focus{background-color:#662b71}.label-info{background-color:#883996}.label-info[href]:hover,.label-info[href]:focus{background-color:#662b71}.label-warning{background-color:#f29446}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ef7816}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#883996;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:0}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:0;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#883996}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:0}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#3a633d;background-color:#a6ecb1;border-color:#89e689}.alert-info hr{border-top-color:#75e174}.alert-info .alert-link{color:#274329}.alert-warning{color:#fff;background-color:#f29446;border-color:#f0662e}.alert-warning hr{border-top-color:#ef5416}.alert-warning .alert-link{color:#e6e6e6}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:0;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#883996;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#883996}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#883996}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#f29446}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{color:#999;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#999}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#883996;border-color:#883996}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#ddb7e4}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#3a633d;background-color:#a6ecb1}a.list-group-item-info,button.list-group-item-info{color:#3a633d}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#3a633d;background-color:#91e89f}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#3a633d;border-color:#3a633d}.list-group-item-warning{color:#fff;background-color:#f29446}a.list-group-item-warning,button.list-group-item-warning{color:#fff}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#fff;background-color:#f0862e}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#fff;border-color:#fff}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:-1px;border-top-right-radius:-1px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a,.panel-title>small,.panel-title>.small,.panel-title>small>a,.panel-title>.small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:-1px;border-top-right-radius:-1px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-right:15px;padding-left:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-left-radius:-1px;border-top-right-radius:-1px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:-1px;border-top-right-radius:-1px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:-1px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:-1px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:-1px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:-1px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:0}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#444}.panel-default>.panel-heading{color:#999;background-color:#444;border-color:#444}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#444}.panel-default>.panel-heading .badge{color:#444;background-color:#999}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#444}.panel-primary{border-color:#883996}.panel-primary>.panel-heading{color:#fff;background-color:#883996;border-color:#883996}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#883996}.panel-primary>.panel-heading .badge{color:#883996;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#883996}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#89e689}.panel-info>.panel-heading{color:#3a633d;background-color:#a6ecb1;border-color:#89e689}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#89e689}.panel-info>.panel-heading .badge{color:#a6ecb1;background-color:#3a633d}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#89e689}.panel-warning{border-color:#f0662e}.panel-warning>.panel-heading{color:#fff;background-color:#f29446;border-color:#f0662e}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#f0662e}.panel-warning>.panel-heading .badge{color:#f29446;background-color:#fff}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#f0662e}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:0}.well-sm{padding:9px;border-radius:0}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none;appearance:none}.tooltip{position:absolute;z-index:1030;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:12px;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:0}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.clearfix,.dl-horizontal dd,.container,.container-fluid,.row,.form-horizontal .form-group,.btn-toolbar,.btn-group-vertical>.btn-group,.nav,.navbar,.navbar-header,.navbar-collapse,.pager,.panel-body{display:block}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after{display:table;content:" "}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (max-width:767px){.visible-xs-block{display:block !important}}@media (max-width:767px){.visible-xs-inline{display:inline !important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table !important}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block !important}}@media (min-width:992px) and (max-width:1239px){.visible-md{display:block !important}table.visible-md{display:table !important}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:992px) and (max-width:1239px){.visible-md-block{display:block !important}}@media (min-width:992px) and (max-width:1239px){.visible-md-inline{display:inline !important}}@media (min-width:992px) and (max-width:1239px){.visible-md-inline-block{display:inline-block !important}}@media (min-width:1240px){.visible-lg{display:block !important}table.visible-lg{display:table !important}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (min-width:1240px){.visible-lg-block{display:block !important}}@media (min-width:1240px){.visible-lg-inline{display:inline !important}}@media (min-width:1240px){.visible-lg-inline-block{display:inline-block !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1239px){.hidden-md{display:none !important}}@media (min-width:1240px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table !important}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}}#cloud-plugins-library{background:#2c2c2c;bottom:45px;left:0;overflow:auto;position:fixed;right:0;top:0;z-index:2}#cloud-plugins-library h1{color:#fff;display:inline-block;font-size:24px;line-height:35px;margin:0 0 18px 0;padding:0}#cloud-plugins-library header .help{background-image:url(../img/icn-help-16x16.png);background-position:center center;background-repeat:no-repeat;display:inline-block;height:21px;width:21px}#cloud-plugins-library header .close{opacity:1;position:absolute;right:10px;top:16px}#cloud-plugins-library .filter{bottom:45px;left:0;position:fixed;top:45px;right:0;width:240px;overflow:auto}#cloud-plugins-library .filter h1{font-weight:2em;text-transform:uppercase;font-weight:bold;margin:0 0 0 10px}#cloud-plugins-library .filter h2{color:white;font-size:15px;text-transform:uppercase;font-weight:bold;margin-top:1em;margin-left:10px}#cloud-plugins-library .filter .control-group .switch{margin:5px 0 5px 15px;position:relative;width:18rem;height:2.5rem;font-size:0}#cloud-plugins-library .filter .control-group .switch input{position:absolute;top:0;z-index:2;opacity:0;cursor:pointer;height:2.5rem;width:5rem;left:5.5rem;margin:0}#cloud-plugins-library .filter .control-group .switch input:checked{z-index:1}#cloud-plugins-library .filter .control-group .switch input:checked+label{opacity:1;cursor:default}#cloud-plugins-library .filter .control-group .switch input:checked~.toggle-outside .toggle-inside{left:.25rem;background:#883996}#cloud-plugins-library .filter .control-group .switch input:not(:checked)+label:hover{opacity:.5}#cloud-plugins-library .filter .control-group .switch input~input:checked~.toggle-outside .toggle-inside{left:2.65rem;background:#4a4a4a}#cloud-plugins-library .filter .control-group .switch label{color:#fff;opacity:.33;transition:opacity .25s ease;cursor:pointer;font-size:14px;line-height:3rem;display:inline-block;width:5rem;height:100%;margin:0;text-align:center}#cloud-plugins-library .filter .control-group .switch label:first-of-type{text-align:left}#cloud-plugins-library .filter .control-group .switch label:last-of-type{margin-left:5.5rem}#cloud-plugins-library .filter .control-group .switch .toggle-outside{height:100%;border-radius:2rem;padding:.25rem;overflow:hidden;transition:.25s ease all;background:#fff;opacity:.8;position:absolute;width:5rem;left:5.5rem}#cloud-plugins-library .filter .control-group .switch .toggle-inside{border-radius:5rem;background:#4a4a4a;position:absolute;transition:.25s ease all;height:2rem;width:2rem}#cloud-plugins-library .filter .control-group select{width:50px}#cloud-plugins-library .filter .control-group input[type='checkbox']{display:none}#cloud-plugins-library .filter .control-group input[type='checkbox']+label{cursor:pointer;background:#222;color:#fff;display:block;height:32px;line-height:32px;width:100%;margin-bottom:1px;padding-left:15px;border:0 solid #f29446}#cloud-plugins-library .filter .control-group input[type='checkbox']+label:hover{background:#000}#cloud-plugins-library .filter .control-group input[type='checkbox']:checked+label{background:#f29446}#cloud-plugins-library .filter .control-group input[type='checkbox']:checked+label:hover{background:#600e6f}#cloud-plugins-library .filter>.form-group .input-clean{margin-right:8px !important}#cloud-plugins-library .filter .categories{width:100%;margin:0 0 15px 0}#cloud-plugins-library .filter .categories li{display:block;background:#222;color:#888;cursor:pointer;font-weight:bold;margin:0 0 1px 0;padding:0 15px;position:relative;transition:all .33s;line-height:32px;height:32px}#cloud-plugins-library .filter .categories li span{float:right}#cloud-plugins-library .filter .categories li.selected{background:#883996;color:#fff}#cloud-plugins-library .filter .categories li:hover{background:#000}#cloud-plugins-library .filter .categories li.selected:hover{background:#600e6f}.cloud-plugins{left:240px;position:fixed;right:0;top:45px;bottom:45px;background:#111 url(../img/watermark.png) 100% 100% no-repeat;overflow-y:auto}.cloud-plugins .plugins-wrapper{width:100%}.cloud-plugins .cloud-plugin{color:#fff;float:left;width:100%;position:relative;min-height:1px;padding-right:15px;padding-left:15px;height:120px;border:5px solid #000;padding:0 !important;background:rgba(255,255,255,0.07);transition:all .33s;cursor:pointer}@media (min-width:768px){.cloud-plugins .cloud-plugin{float:left;width:50%}}@media (min-width:992px){.cloud-plugins .cloud-plugin{float:left;width:50%}}@media (min-width:1240px){.cloud-plugins .cloud-plugin{float:left;width:33.33333333%}}.cloud-plugins .cloud-plugin:hover{background-color:black}.cloud-plugins .cloud-plugin:hover .description{background:rgba(0,0,0,0.8)}.cloud-plugins .cloud-plugin:hover .description hr{border-color:#f29446}.cloud-plugins .cloud-plugin:hover figure{top:-5px}.cloud-plugins .cloud-plugin:hover div.demo-plugin{opacity:0}.cloud-plugins .cloud-plugin:hover span.price{position:absolute;top:25px;margin-left:98px;padding:3px 0;transform:rotate(0deg) scale(1.3);width:80px}.cloud-plugins .cloud-plugin:hover span.licensed{position:absolute;top:25px;margin-left:98px;padding:3px 0;transform:rotate(0deg) scale(1.3);width:113px;text-align:center}.cloud-plugins .cloud-plugin:hover span.coming-soon{position:absolute;top:25px;margin-left:98px;padding:3px 0;transform:rotate(0deg) scale(1.3);width:113px;text-align:center}.cloud-plugins .cloud-plugin:hover span.price-container{margin-left:170px;width:200px}.cloud-plugins .cloud-plugin figure{height:100px;position:absolute;top:10px;left:20px;margin:0;transition:all .33s linear;display:flex;flex-direction:column;justify-content:center}.cloud-plugins .cloud-plugin figure img{height:auto;margin:0 auto 3px;width:auto;pointer-events:none;max-width:256px;max-height:64px}.cloud-plugins .cloud-plugin span.price{transition:all .33s linear}.cloud-plugins .cloud-plugin span.price-container{transition:all .33s linear}.cloud-plugins .cloud-plugin .cloud-plugin-border{border:1px solid #333;width:100%;height:100%}.cloud-plugins .cloud-plugin .description{transition:all .33s;display:block;line-height:1.5;overflow:hidden;position:absolute;top:1px;right:1px;bottom:1px;left:80px;background:rgba(33,33,33,0.66)}.cloud-plugins .cloud-plugin .description .no_description{color:rgba(255,255,255,0.25)}.cloud-plugins .cloud-plugin .description hr{transition:all .33s;border:none;border-bottom:1px solid #883996;margin:0;position:absolute;top:50%;left:0;width:calc(100% - 5px)}.cloud-plugins .cloud-plugin .description .title{display:block;text-transform:uppercase;font-weight:bold;overflow:hidden;padding:8px 10px 1px 10px}.cloud-plugins .cloud-plugin .description .author{display:block;padding:1px 10px}.cloud-plugins .cloud-plugin .description p{position:absolute;left:10px;right:10px;top:75%;transform:translateY(-50%);height:2.4em;line-height:1.25em;overflow:hidden}.cloud-plugins .cloud-plugin .actions{font-size:11px;font-weight:bold;text-transform:uppercase}.cloud-plugins .cloud-plugin .actions>span{cursor:pointer}.cloud-plugins .cloud-plugin .actions>span:hover{text-decoration:underline}.cloud-plugins .cloud-plugin .actions span.duplicate{margin:0 6px}.cloud-plugins h2{margin-left:23px;font-size:20px;color:#ccc}.cloud-plugins h3{color:#ccc}.cloud-plugins .featured-plugins{position:relative;height:360px;width:100%;display:block}.cloud-plugins .featured-plugins.double,.cloud-plugins .featured-plugins.single{height:490px !important}.cloud-plugins .featured-plugins.double .box,.cloud-plugins .featured-plugins.single .box{transition:all 0s !important}.cloud-plugins .featured-plugins.double .carousel,.cloud-plugins .featured-plugins.single .carousel{height:485px !important}.cloud-plugins .featured-plugins.double .carousel .slick-list,.cloud-plugins .featured-plugins.single .carousel .slick-list{height:485px !important}.cloud-plugins .featured-plugins.double .box{margin:30px 80px !important}.cloud-plugins .featured-plugins.single .box{margin:30px 30px !important}.cloud-plugins .featured-plugins .carousel{position:absolute;top:0;bottom:0;right:90px;left:90px;height:380px !important}.cloud-plugins .featured-plugins .carousel .slick-list{height:380px !important}.cloud-plugins .featured-plugins .carousel .featured{cursor:pointer;margin-top:40px}.cloud-plugins .featured-plugins .carousel .featured .box{padding:4px;border:1px solid #737373;height:300px;opacity:.4;margin:0 24px}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box{border:1px solid #444;text-align:center;height:290px}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .price-container{position:absolute;top:0;left:0;width:90px;height:90px;overflow:hidden;color:#fff}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .price{background-color:#883996;border:1px solid #883996;position:absolute;top:9px;left:-30px;padding:3px 0;transform:rotate(-45deg);min-width:108px;text-align:center}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .price:before{content:"€ "}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .licensed{background-color:#883996;border:1px solid #883996;position:absolute;top:16px;left:-30px;padding:3px 27px;font-size:12px;transform:rotate(-45deg)}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .licensed:before{content:"PURCHASED"}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box h2{color:#c3c3c3;text-align:center}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .img-container{height:113px;width:100%}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .img-container img{vertical-align:middle;transition:all 500ms ease;width:auto;height:auto;margin:auto;display:block;max-height:113px;padding:0 20px;max-width:100%}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box p{color:#e0e0e0;padding:5px 5px 0 5px;height:59px}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box button{background-color:transparent;color:#ff3838;border:1px solid #ff3838;text-transform:uppercase;padding:8px 20px}.cloud-plugins .featured-plugins .carousel .featured.slick-center{height:390px}.cloud-plugins .featured-plugins .carousel .featured.slick-center .box{opacity:1;transform:scale(1.2) translate(0, 0);transition:all 500ms ease}.cloud-plugins .featured-plugins .carousel .slick-arrow{width:60px;height:60px;top:200px}.cloud-plugins .featured-plugins .carousel .slick-arrow:before{font-size:60px}.cloud-plugins .featured-plugins .carousel .slick-prev{left:-70px !important}.cloud-plugins .featured-plugins .carousel .slick-next{right:-70px !important}.plugin-container .status-container{position:absolute;top:0;left:0;right:2px;text-align:right;padding-right:2px}.plugin-container .status{padding:1px 4px;text-align:center;text-transform:uppercase;font-weight:bold;font-size:9px;margin-left:1px}.plugin-container .status.installed{border:1px solid #883996;background-color:#883996}.plugin-container .status.installed:after{content:"installed"}.plugin-container .status.outdated{border:1px solid #f29446;background-color:#f29446;color:#212121}.plugin-container .status.outdated:after{content:"new version"}.plugin-container .price-container{position:absolute;top:0;left:0;width:90px;height:90px;overflow:hidden}.plugin-container .price{background-color:#883996;border:1px solid #883996;position:absolute;top:9px;left:-25px;padding:3px 0;transform:rotate(-45deg);min-width:108px;text-align:center}.plugin-container .price:before{content:"€ "}.plugin-container .licensed{background-color:#883996;border:1px solid #883996;position:absolute;top:21px;left:-30px;padding:3px 27px;font-size:14px;transform:rotate(-45deg)}.plugin-container .licensed:before{content:"PURCHASED"}.plugin-container .coming-soon{background-color:#883996;border:1px solid #883996;position:absolute;top:26px;left:-35px;padding:3px 27px;font-size:12px;transform:rotate(-45deg);width:135px}.plugin-container .coming-soon:before{content:"TRIAL ONLY"}.plugin-container .status.buildEnvironment{border:1px solid #ff2927;background-color:#ff2927}.plugin-container .status.buildEnvironment:after{content:"local"}.plugin-container .status.buildEnvironment.labs:after{content:"labs"}.plugin-container .status.buildEnvironment.dev:after{content:"dev"}.plugin-container .status.buildEnvironment.prod{display:none}.plugin-container .status.buildEnvironment.prod:after{content:""}.plugin-container .demo{border:1px solid #ff2927;background-color:#ff2927}.plugin-container .demo:after{content:"demo"}.plugin-container .description-price-container{position:absolute;top:-17px;left:-17px;overflow:hidden;height:200px}.plugin-container .description-price-container .description-price{background-color:#883996;border:1px solid #883996;position:relative;top:34px;left:-57px;padding:3px 0;transform:rotate(-45deg);min-width:250px;text-align:center;font-size:34px;z-index:11}.plugin-container .description-price-container .description-price:before{content:"€ "}.plugin-container .description-price-container .description-licensed{background-color:#883996;border:1px solid #883996;position:relative;top:50px;left:-60px;padding:3px 0;transform:rotate(-45deg);min-width:280px;text-align:center;font-size:34px;z-index:1}.plugin-container .description-price-container .description-licensed:before{content:"PURCHASED"}.plugin-container .description-price-container .description-coming-soon{background-color:#883996;border:1px solid #883996;position:relative;top:50px;left:-70px;padding:3px 0;transform:rotate(-45deg);min-width:280px;text-align:center;font-size:26px;z-index:1}.plugin-container .description-price-container .description-coming-soon:before{content:"TRIAL ONLY"}.buy-button{margin-top:-20px;position:relative}.buy-button iframe{position:absolute;top:-17px;left:0;min-width:115px;z-index:1}.shopify-fake-button{position:absolute;top:3px;left:0;z-index:0;background-color:#c224b0;font-family:Arial;font-size:12px;opacity:.25;min-width:115px;min-height:34px;padding:9px 15px;text-align:center}#bank-library{bottom:45px;left:0;overflow-x:hidden;overflow-y:auto;position:absolute;right:0;top:0;z-index:2}#bank-library .ui-draggable-dragging{cursor:webkit-dragging !important}#bank-library .mod-banks-drag-item{background:#f29446;color:white;font-weight:bold;z-index:10000;box-shadow:0 5px 15px black;opacity:.5}#bank-library .mod-banks-drag-item .img{display:block;height:auto;margin:0 auto;text-align:center;width:100%;background:url(../img/background.jpg);background-size:auto 100%;background-position:center center;box-shadow:inset 0 3px 6px black;border:1px solid #000}#bank-library .mod-banks-drag-item .title{display:block;line-height:32px;font-weight:bold;overflow:hidden;position:relative;padding-left:5px}#bank-library span.move{background-image:url(../img/move.png);background-position:left center;background-repeat:no-repeat;display:block;float:left;height:32px;left:0;cursor:move !important;margin:-1px 6px 0 0;width:24px}#bank-library span.remove{cursor:pointer;float:right;width:32px;height:32px;display:block;background-image:url(../img/icons/25/delete.png);background-repeat:no-repeat;background-position:50% 50%;transition:background-color .33s}#bank-library span.remove:hover{background-color:#f29446}#bank-library span.title{display:block;float:left;height:32px;overflow:hidden;max-width:70%;white-space:nowrap}#bank-library input::selection{background:#883996 !important;color:white}#bank-library .icon-add{width:1em;height:1em;display:inline-block;font-size:inherit;vertical-align:baseline}#bank-library .icon-add i{position:absolute;width:1em;margin-top:.3em;border-top:.4em solid #888}#bank-library .icon-add b{position:absolute;height:1em;margin-left:.3em;border-left:.4em solid #888}#bank-library #bank-list{left:0;list-style:none;margin:0;padding:0;position:fixed;bottom:45px;overflow-y:auto;top:45px;width:20%;background:#2c2c2c}#bank-library #bank-list>div.add-bank{color:#fff;cursor:pointer;padding-left:5px;line-height:32px;text-transform:uppercase;transition:all .33s}#bank-library #bank-list>div.add-bank:hover{background:#f29446}#bank-library #bank-list>div.add-bank .icon-add{display:inline-block;width:24px;margin-right:6px;padding-left:5px;box-sizing:border-box}#bank-library #bank-list>div.selected span.move{background-position:right center}#bank-library #bank-list>div.selected span.remove{background-position:right center}#bank-library #bank-list .edit-bank{color:#555;width:100% !important;height:30px;margin:1px;padding-left:.5em;background-color:#fff;background-image:none;border:0 solid #ccc;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out}#bank-library #bank-list div{background:#222;color:#888;cursor:pointer;font-weight:bold;margin:0 0 1px 0;content:\002B;position:relative;transition:all .33s}#bank-library #bank-list div.selected{background:#883996;color:#fff}#bank-library #bank-list div:hover{background:#000}#bank-library #bank-list div.selected:hover{background:#600e6f}#bank-library #bank-list .bank-item{display:block;background:#222;color:#888;line-height:32px;height:32px;cursor:pointer;font-weight:bold;margin:0 0 1px 0;padding-left:5px;position:relative}#bank-library #bank-list .bank-item .remove{border-radius:0}#bank-library #bank-list .filter_{border-color:#111;border-style:dotted;border-width:0 1px 0 0;bottom:46px;left:0;padding-top:18px;position:fixed;top:0;right:0;width:240px;overflow:auto}#bank-library #bank-list .filter_ h1{font-weight:2em;text-transform:uppercase;font-weight:bold;margin:0 0 0 10px}#bank-library #bank-list .filter_ h2{color:white;font-size:15px;text-transform:uppercase;font-weight:bold;margin-top:1em;margin-left:10px}#bank-library #bank-list .filter_ .control-group{float:left;margin-right:9px;margin-left:10px}#bank-library #bank-list .filter_ .control-group label{color:#fff;display:inline-block;font-size:12px;font-weight:bold}#bank-library #bank-list .filter_ .control-group select{width:50px}#bank-library #bank-list .filter_>.form-group{padding:15px}#bank-library #bank-title{background:#111;left:20%;height:92px;margin:0;padding:0;position:fixed;text-align:left;top:45px;width:50%}#bank-library #bank-title h1{font-size:2em;padding:0;max-height:2em;display:block;margin:.5em .8em 0 .8em;padding-bottom:.2em;color:white;white-space:nowrap;overflow:hidden}#bank-library #bank-title form{margin:0 2em;color:#666}#bank-library #bank-title h2{margin:4px 0 0 14px;display:block;text-transform:initial !Important;color:white;font-size:12px;line-height:20px;text-align:left}#bank-library #bank-title small{font-size:10px;color:#bbb;text-align:left;display:block;margin-left:14px}#bank-library #bank-title select{display:block;float:left;height:22px;margin:5px 2.5%;width:20%;font-size:10px}#bank-library #bank-pedalboards{background:#111;bottom:46px;left:20%;list-style:none;margin:0;overflow-x:hidden;overflow-y:auto;padding:0 0 200px 0;position:fixed;top:164px;width:50%}#bank-library #bank-pedalboards>div{margin:0 10px 20px 20px}#bank-library #bank-pedalboards .pedalboard{color:#fff;cursor:-webkit-grab !important;cursor:grab !important;width:100%;transition:all .33s}#bank-library #bank-pedalboards .pedalboard:active{cursor:-webkit-grabbing !important;cursor:grabbing !important}#bank-library #bank-pedalboards .pedalboard:hover{background-color:#883996}#bank-library #bank-pedalboards .pedalboard .info{height:32px;padding-left:5px;background:rgba(255,255,255,0.07)}#bank-library #bank-pedalboards .pedalboard .title{display:block;line-height:32px;font-weight:bold;overflow:hidden;position:relative}#bank-library #bank-pedalboards .pedalboard .img{display:inline-block;float:left;height:auto;margin:0 9px 0 0;text-align:center;width:100%;background:url(../img/background.jpg);background-size:auto 100%;background-position:center center;box-shadow:inset 0 3px 6px black;border:1px solid #000}#bank-library #bank-pedalboards .mode{border:none;margin:22px 0 18px 0;text-align:center}#bank-library #bank-edit{position:fixed;display:block;background:#111 url(../img/watermark.png) 100% 100% no-repeat;width:80%;bottom:45px;top:45px;left:20%}#bank-library #bank-pedalboards-search{bottom:45px;right:0;overflow:auto;position:fixed;top:0;width:30%;z-index:6}#bank-library #bank-pedalboards-search h2{text-transform:uppercase;color:white;font-size:1.5em;font-weight:bold;float:left}#bank-library #bank-pedalboards-search .box{background:#111;padding:0;box-sizing:border-box}#bank-library #bank-pedalboards-search input{width:50%;padding-left:1em}#bank-library #bank-pedalboards-search .remove{display:none !important}#bank-library #bank-pedalboards-search label{color:#fff;font-weight:bold;text-transform:uppercase;float:left;line-height:45px;padding-left:10px}#bank-library #bank-pedalboards-search .control-group{margin-bottom:10px;padding:0 2%;position:relative;width:29.3%}#bank-library #bank-pedalboards-search input[type="search"]{width:50% !important;float:right;margin:5px}#bank-library #bank-pedalboards-search select{width:100%}#bank-library #bank-pedalboards-search .input-clean{background:#333;color:#fff;cursor:pointer;font-size:8px;height:18px;left:auto !important;position:absolute;right:8px !important;text-align:center;text-transform:uppercase;top:31px !important;width:18px;border-radius:100%}#bank-pedalboards-mode{background:#333;left:30%;height:30px;margin:0;padding:20px 0 0 0;position:absolute;text-align:center;top:68px;width:45%}#bank-pedalboards-mode .grid{background:url(../img/icn-grid-list.png) no-repeat left bottom;display:inline-block;height:14px;width:14px}#bank-pedalboards-mode .grid.selected{background-position:left top}#bank-pedalboards-mode .list{background:url(../img/icn-grid-list.png) no-repeat right bottom;display:inline-block;height:14px;width:14px}#bank-pedalboards-mode .list.selected{background-position:right top}.pedalboard .addressings{width:100%}.pedalboard .addressings .footswitch{background:#2b2b2b url(../img/footswitch-free.png) no-repeat center top;float:left;height:20px;width:25%}.pedalboard .addressings .footswitch.addressed{background:#2b2b2b url(../img/footswitch-addressed.png) no-repeat center top}#bank-pedalboards-result{position:fixed;bottom:45px;top:45px;overflow:auto;width:30%;background:#111}#bank-pedalboards-result .pedalboard{color:#fff;background:rgba(255,255,255,0.07);cursor:-webkit-grab;cursor:grab;margin:0 20px 20px 10px;transition:all .33s}#bank-pedalboards-result .pedalboard:active{cursor:-webkit-grabbing !important;cursor:grabbing !important}#bank-pedalboards-result .pedalboard:hover{background-color:#883996}#bank-pedalboards-result .pedalboard .img{display:block;height:auto;margin:0 auto;text-align:center;width:100%;background:url(../img/background.jpg);background-size:auto 100%;background-position:center center;box-shadow:inset 0 3px 6px black;border:1px solid #000}#bank-pedalboards-result .pedalboard .img img{max-width:100%}#bank-pedalboards-result .pedalboard .title{display:block;line-height:32px;font-weight:bold;overflow:hidden;position:relative;padding-left:5px}#bank-pedalboards-result .pedalboard .description{display:block;font-size:11px;height:27px;line-height:1.5;overflow:hidden}#file-manager-library{background:#2c2c2c url(../img/watermark.png) 100% 100% no-repeat;top:0;bottom:46px;left:0;right:0;overflow-x:hidden;overflow-y:auto;position:absolute;z-index:2}#file-manager-wrapper{display:flex;position:fixed;top:45px;bottom:55px;left:2em;right:2em;overflow:auto;padding-top:1%}#file-manager-wrapper iframe{flex:1;border:none}#pedalboards-library{background:#2c2c2c;bottom:45px;left:0;overflow-x:hidden;overflow-y:auto;position:absolute;right:0;top:0;z-index:2}#pedalboards-library h1{color:#fff;display:inline-block;font-size:24px;line-height:35px;margin:0 0 18px 0;padding:0}#pedalboards-library header .form-group{display:inline-block;float:right;margin-top:5px;margin-right:5px}#pedalboards-library header .form-group .form-control{width:200px !important}#pedalboards-library header .form-group .input-clean{line-height:20px;margin-right:12px !important}#pedalboards-library header .view-modes{float:right;margin-right:12px;cursor:pointer}#pedalboards-library header .view-modes .view-mode{font-size:130%}#pedalboards-library header .view-modes .view-mode.selected{color:#fff}#pedalboards-library .filter{border-color:#111;border-style:dotted;border-width:0 1px 0 0;bottom:45px;left:0;position:fixed;right:0;top:45px;width:240px}#pedalboards-library .filter .control-group{float:left;margin-right:9px}#pedalboards-library .filter .control-group label{color:#fff;display:inline-block;font-size:12px;font-weight:bold;width:165px}#pedalboards-library .filter .control-group select{width:50px}#pedalboards-library .pedalboards{left:0;position:fixed;right:0;top:45px;bottom:45px;overflow:auto;background:#111 url(../img/watermark.png) 100% 100% no-repeat;padding-top:1%}#pedalboards-library .pedalboards .pedalboard{color:#fff;float:left;margin:0 0 1% 1%;width:18.8%;position:relative;background-color:transparent;background-image:url(../img/background.jpg);background-position:center center;background-repeat:repeat;background-size:auto 100%;transition:all .33s;cursor:pointer}#pedalboards-library .pedalboards .pedalboard:hover .info{background-color:#883996}#pedalboards-library .pedalboards .pedalboard .img{box-shadow:inset 0 3px 6px black;border:1px solid #000;display:block;width:100%;height:10vw;overflow:hidden;text-align:center;background-image:url(../img/loading-pedalboard.gif);background-position:center center;background-repeat:no-repeat;background-size:auto;position:relative}#pedalboards-library .pedalboards .pedalboard .img.loaded{background-size:contain}#pedalboards-library .pedalboards .pedalboard .img.broken{background-image:url(../img/icons/broken_image.svg);background-size:auto 50%}#pedalboards-library .pedalboards .pedalboard.broken .img::after{content:"";position:absolute;top:0;left:0;right:0;bottom:0;z-index:10;background-color:rgba(17,17,17,0.5);background-image:url(../img/icons/broken_pedal.svg);background-size:auto 50%;background-repeat:no-repeat;background-position:center}#pedalboards-library .pedalboards .pedalboard .info{background-color:#2c2c2c;transition:background-color .33s}#pedalboards-library .pedalboards .pedalboard .title{display:block;line-height:32px;font-weight:bold;overflow:hidden;position:relative;height:32px;left:5px;width:calc(100% - 32px);text-overflow:ellipsis;white-space:nowrap}#pedalboards-library .pedalboards .pedalboard .description{display:block;font-size:12px;height:36px;line-height:1.5;overflow:hidden}#pedalboards-library .pedalboards .pedalboard .actions{font-weight:bold;text-transform:uppercase;position:absolute;top:0;right:0}#pedalboards-library .pedalboards .pedalboard .actions .js-remove{width:32px;height:32px;background-image:url(../img/icons/25/delete.png);background-repeat:no-repeat;background-position:50% 50%;transition:background-color .33s}#pedalboards-library .pedalboards .pedalboard .actions .js-remove:hover{background-color:#f29446}#pedalboards-library .pedalboards .pedalboard .actions>span{cursor:pointer}#pedalboards-library .pedalboards .pedalboard .actions>span:hover{text-decoration:underline}#pedalboards-library .pedalboards .pedalboard .actions>span.duplicate{margin:0 6px}#pedalboards-library .pedalboards.list-selected .img{display:none}#nprogress{pointer-events:none;z-index:103100}#nprogress .bar{background:#883996;position:fixed;z-index:103100;top:45px;left:0;opacity:.5;width:100%;height:2px}#nprogress .peg{display:none}#nprogress .spinner{display:none;position:fixed;z-index:1031;top:45px;right:0;background:rgba(0,0,0,0.5);padding:6px;box-sizing:border-box;transform:translate(-50% -50%)}#nprogress .spinner-icon{width:32px;height:32px;box-sizing:border-box;border:solid 3px transparent;border-top-color:#f29446;border-left-color:rgba(242,148,70,0.66);border-bottom-color:rgba(242,148,70,0.33);border-radius:50%;-webkit-animation:nprogress-spinner 400ms linear infinite;animation:nprogress-spinner 400ms linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .spinner,.nprogress-custom-parent #nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.mod-pedal-settings .mod-address.addressed,.mod-pedal-settings .preset-btn-assign-all.addressed{background-color:#a144b2}.mod-pedal-settings-address .mod-box .form-container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.mod-pedal-settings-address .mod-box .form-container .btn.js-type,.mod-pedal-settings-address .mod-box .form-container .btn.advanced-toggle{border:solid #333 1px}.mod-pedal-settings-address .mod-box .form-container .main-form-container{width:691px;padding-right:3px}.mod-pedal-settings-address .mod-box .form-container .main-form-container label{text-align:center}.mod-pedal-settings-address .mod-box .form-container .main-form-container .btn.js-type.selected{background-color:#333;color:white}.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike{display:flex;flex-basis:100%;align-items:center;color:black;margin:6px 0}.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike:before,.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike:after{content:"";flex-grow:1;background:black;height:3px;font-size:0;line-height:0}.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike:before{margin-right:8px}.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike:after{margin-left:8px}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table{width:100%}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:first-child{background-color:#f66}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(2){background-color:#ff6}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(3){background-color:#6ff}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(4){background-color:#bb7cd5}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(5){background-color:#f5a77e}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(6){background-color:#6f6}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(7){background-color:#f6f}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(8){background-color:#8080ff}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th,.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table td{border:solid #333 1px;text-align:center}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table td{padding:5px;cursor:pointer}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table td.selected{background-color:#883996;color:white;box-shadow:3px 3px 3px #999}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table td.disabled{cursor:auto;color:#999;background-color:#d9d9d9;box-shadow:inset 0 0 3px #555}.mod-pedal-settings-address .mod-box .form-container .main-form-container .midi-custom-uri{color:#883996}.mod-pedal-settings-address .mod-box .form-container .buttons-container{padding-right:3px;padding-bottom:18px}.mod-pedal-settings-address .mod-box .form-container .advanced-container{border-left:1px solid #eee;padding-left:12px;margin-left:12px}.mod-pedal-settings-address .mod-box .form-container .advanced-container .cv-op-mode label{width:100% !important}.mod-pedal-settings-address .mod-box .form-container .advanced-container .cv-op-mode .mod-controls{width:155px !important}.mod-cv-output .output-cv-checkbox{display:flex;position:absolute;top:45px;left:15px}.mod-cv-output .output-cv-checkbox .checkbox-container{position:relative;padding-left:45px;cursor:pointer;font-size:22px;height:35px;width:35px}.mod-cv-output .output-cv-checkbox .checkbox-container .checkmark{position:absolute;top:0;left:0;height:35px;width:35px;background-color:#eee}.mod-cv-output .output-cv-checkbox .checkbox-container .checkmark:after{content:"";position:absolute;display:none;left:13px;top:7px;width:10px;height:17px;border:solid white;border-width:0 4px 4px 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]{position:absolute;opacity:0;cursor:pointer;z-index:5;width:100%;height:100%;top:0;left:0}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]:disabled{cursor:not-allowed}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]:checked~.checkmark{background-color:#883996}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]:checked~.checkmark:after{display:block}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]:disabled~.checkmark{background-color:#999}.mod-cv-output .output-cv-checkbox input[type=text]{font-size:20px}.mod-cv-output .output-cv-checkbox ::selection{background:#883996 !important;color:white !important}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-semibold-webfont.eot');src:url('../fonts/cooper/cooperhewitt-semibold-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-semibold-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-semibold-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-semibold-webfont.ttf') format('truetype');font-weight:600;font-style:normal;font-display:swap}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-light-webfont.eot');src:url('../fonts/cooper/cooperhewitt-light-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-light-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-light-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-light-webfont.ttf') format('truetype');font-weight:300;font-style:normal;font-display:swap}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-lightitalic-webfont.eot');src:url('../fonts/cooper/cooperhewitt-lightitalic-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-lightitalic-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-lightitalic-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-lightitalic-webfont.ttf') format('truetype');font-weight:300;font-style:italic;font-display:swap}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-bookitalic-webfont.eot');src:url('../fonts/cooper/cooperhewitt-bookitalic-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-bookitalic-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-bookitalic-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-bookitalic-webfont.ttf') format('truetype');font-weight:400;font-style:italic;font-display:swap}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-book-webfont.eot');src:url('../fonts/cooper/cooperhewitt-book-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-book-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-book-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-book-webfont.ttf') format('truetype');font-weight:400;font-style:normal;font-display:swap}*{outline:0 !important}html,body{height:100%;overflow:hidden}body{background:#111;min-width:1024px}body.ios{position:fixed;min-width:980px}*{font-family:"cooper hewitt",Sans-serif !important}.unpadded{padding:0 !important}.unpadded-r{padding:0 0 0 7px}.unpadded-l{padding:0 7px 0 0}.ui-draggable:active{cursor:-webkit-grabbing !important;cursor:grabbing !important}.form-control{width:100% !important}#wrapper{bottom:0;left:0;min-width:980px;overflow:hidden;position:absolute;right:0;top:0}hr.dotted{border:1px dotted #444;margin:2px 0 3px 0}.preset-list{width:100%;height:200px !important}.input-clean{background:#333;color:#fff;cursor:pointer;height:18px;left:auto !important;position:absolute;right:0;margin-right:18px !important;text-align:center;text-transform:uppercase;margin-top:-26px !important;width:18px;border-radius:100%}.input-clean:after{content:"×";content:"\00d7";font-size:14px;font-weight:bold}.form-control::selection{background:#883996 !important;color:white !important}#notifications{position:absolute;top:55px;right:0;width:320px;z-index:100000}#notifications>section{border-radius:0;margin-bottom:0 !important;opacity:.96;cursor:pointer}#notifications .progressbar{border:0;height:8px;overflow:hidden;position:absolute;left:8px;bottom:0;width:306px}#notifications .progressbar .progressbar-value{background-color:rgba(136,57,150,0.7);height:90%;margin:0}#main-menu{background:#111;bottom:0;color:#fff;height:45px;left:0;position:absolute;right:0;z-index:10}#main-menu>div{cursor:pointer;float:left}#main-menu>div.icon{height:45px;width:45px}#main-menu>div.icon>img{margin:5px 0 0 6px}#main-menu>div.icon.selected{background:#000}#main-menu>div.icon>a{display:block;height:45px;width:45px}#main-menu #mod-plugins{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-45px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-plugins:hover{background-color:#000;background-position:-45px bottom}#main-menu #mod-plugins.selected{background-color:#000;background-position:-45px bottom}#main-menu #mod-cloud-plugins{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-315px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-cloud-plugins:hover{background-color:#000;background-position:-315px bottom}#main-menu #mod-cloud-plugins.selected{background-color:#000;background-position:-315px bottom}#main-menu #mod-pedalboard{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-90px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-pedalboard:hover{background-color:#000;background-position:-90px bottom}#main-menu #mod-pedalboard.selected{background-color:#000;background-position:-90px bottom}#main-menu #mod-bank{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-135px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-bank:hover{background-color:#000;background-position:-135px bottom}#main-menu #mod-bank.selected{background-color:#000;background-position:-135px bottom}#main-menu #mod-file-manager{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-180px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-file-manager:hover{background-color:#000;background-position:-180px bottom}#main-menu #mod-file-manager.selected{background-color:#000;background-position:-180px bottom}#main-menu #banks-saving{margin:21px 0 0 6px;font-size:10px;color:#666;display:none}#main-menu #mod-show-midi-port,#main-menu #mod-transport-icon,#main-menu .mod-bypass,#main-menu #mod-cpu,#main-menu #mod-ram,#main-menu #mod-xruns,#main-menu #mod-buffersize,#main-menu #mod-cpu-stats{background-position:8px 50%;background-repeat:no-repeat;padding-left:40px;padding-right:12px;color:#fff;float:right;font-size:11px;font-weight:bold;line-height:4.5;text-transform:uppercase;width:auto !important;transition:all .33s}#main-menu #mod-show-midi-port:hover,#main-menu #mod-transport-icon:hover,#main-menu .mod-bypass:hover,#main-menu #mod-cpu:hover,#main-menu #mod-ram:hover,#main-menu #mod-xruns:hover,#main-menu #mod-buffersize:hover,#main-menu #mod-cpu-stats:hover{background-color:#000}#main-menu #mod-show-midi-port.selected,#main-menu #mod-transport-icon.selected,#main-menu .mod-bypass.selected,#main-menu #mod-cpu.selected,#main-menu #mod-ram.selected,#main-menu #mod-xruns.selected,#main-menu #mod-buffersize.selected,#main-menu #mod-cpu-stats.selected{background-color:#333}#main-menu #mod-show-midi-port{background-image:url(../img/icons/25/midi.png)}#main-menu #mod-transport-icon{background-image:url(../img/icons/25/stop.png);font-family:monospace;width:10.5em !important;text-align:right}#main-menu #mod-transport-icon.playing{background-image:url(../img/icons/25/transport.png)}#main-menu .mod-bypass{background-image:url(../img/icons/25/bypass.png)}#main-menu .mod-bypass.bypassed{background-color:#883996}#main-menu #mod-cpu,#main-menu #mod-ram{cursor:default;width:115px !important;padding:0 !important;background:linear-gradient(to right, #1e5799 0, #2989d8 69%, #ff6430 78%, #ff6430 93%, #c40000 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#1e5799', endColorstr='#c40000', GradientType=1)}#main-menu #mod-cpu .progress-title{background-image:url(../img/icons/25/cpu.png);background-position:0 50%;background-repeat:no-repeat;padding-left:30px}#main-menu #mod-ram .progress-title{background-image:url(../img/icons/25/ram.png);background-position:0 50%;background-repeat:no-repeat;padding-left:30px}#main-menu #mod-xruns{background-image:url(../img/icons/25/cpu.png);width:115px !important}#main-menu #mod-cpu-stats{background-image:url(../img/icons/25/cpu.png);width:155px !important;text-transform:none}#main-menu #mod-buffersize{background-image:url(../img/icons/25/cpu.png);width:125px !important}#main-menu .progress{height:45px;margin-bottom:0;overflow:hidden;float:right;background-color:#111;-webkit-transition:width 1s;transition:width 1s}#main-menu .progress .bar{float:right;background:#222 !important}#main-menu .progress-title{position:absolute;top:0;height:45px;margin:0;margin-left:1em;color:white}#main-menu .status{background-color:#111;background-position:3px 50%;background-size:36px;background-repeat:no-repeat;border-color:#2b2b2b;border-style:solid;border-width:0 0 0 1px;float:right}#main-menu #mod-settings{font-family:fontello !important;font-size:28px;padding-top:3px;padding-left:10px;position:relative;color:#fff;opacity:.66;cursor:pointer;transition:all .33s}#main-menu #mod-settings:after{content:'\e802'}#main-menu #mod-settings:hover{opacity:1}#main-menu #mod-update{background-image:url(../img/icons/36/update.png)}#main-menu #mod-update.uptodate{background-image:url(../img/icons/36/ok.png)}#main-menu #mod-status{background-image:url(../img/icons/36/disconnect.png)}#main-menu #mod-status.online{background-image:url(../img/icons/36/disconnect.png)}#main-menu #mod-devices{background-image:url(../img/icons/36/control-chain.png)}#cloud_install_all{background-image:url(../img/icons/36/save.png);float:right;font-size:21px}#cloud_update_all{background-image:url(../img/icons/36/update.png);float:right;font-size:21px}header#pedalboard-actions,#bank-library header,#cloud-plugins-library header,#pedalboards-library header,#file-manager-library header{display:block;left:0;height:45px;line-height:45px;position:fixed;right:0;top:0;box-shadow:0 1px 10px rgba(0,0,0,0.1);z-index:5;background-color:#111;background-position:3px 50%;background-size:36px;background-repeat:no-repeat;background-image:url(../img/icons/36/mod.png);padding-left:45px}header#pedalboard-actions .help,#bank-library header .help,#cloud-plugins-library header .help,#pedalboards-library header .help,#file-manager-library header .help{background-image:url(../img/icn-help-16x16.png);background-position:center center;background-repeat:no-repeat;display:inline-block;height:21px;width:21px}header#pedalboard-actions .close,#bank-library header .close,#cloud-plugins-library header .close,#pedalboards-library header .close,#file-manager-library header .close{opacity:1;position:absolute;right:10px;top:16px}header#pedalboard-actions .drop-menu,#bank-library header .drop-menu,#cloud-plugins-library header .drop-menu,#pedalboards-library header .drop-menu,#file-manager-library header .drop-menu{position:absolute;display:none;list-style-type:none;padding:0;background-color:rgba(0,0,0,0.8);color:#999;width:150px}header#pedalboard-actions .drop-menu:hover,#bank-library header .drop-menu:hover,#cloud-plugins-library header .drop-menu:hover,#pedalboards-library header .drop-menu:hover,#file-manager-library header .drop-menu:hover{display:block}header#pedalboard-actions .drop-menu li,#bank-library header .drop-menu li,#cloud-plugins-library header .drop-menu li,#pedalboards-library header .drop-menu li,#file-manager-library header .drop-menu li{padding:5px 20px}header#pedalboard-actions .drop-menu li:hover,#bank-library header .drop-menu li:hover,#cloud-plugins-library header .drop-menu li:hover,#pedalboards-library header .drop-menu li:hover,#file-manager-library header .drop-menu li:hover{color:white;background-color:#883996}header#pedalboard-actions .drop-menu .sub-menu,#bank-library header .drop-menu .sub-menu,#cloud-plugins-library header .drop-menu .sub-menu,#pedalboards-library header .drop-menu .sub-menu,#file-manager-library header .drop-menu .sub-menu{display:none;list-style-type:none;padding:0;position:absolute;left:150px;height:14em;overflow-y:auto;overflow-x:hidden;margin-top:-50px;background-color:rgba(0,0,0,0.8);width:15em;border-left:3px solid #666}header#pedalboard-actions .drop-menu .sub-menu:hover,#bank-library header .drop-menu .sub-menu:hover,#cloud-plugins-library header .drop-menu .sub-menu:hover,#pedalboards-library header .drop-menu .sub-menu:hover,#file-manager-library header .drop-menu .sub-menu:hover{display:block}header#pedalboard-actions .menu-trigger:hover+.drop-menu,#bank-library header .menu-trigger:hover+.drop-menu,#cloud-plugins-library header .menu-trigger:hover+.drop-menu,#pedalboards-library header .menu-trigger:hover+.drop-menu,#file-manager-library header .menu-trigger:hover+.drop-menu{display:block}header#pedalboard-actions .sub-menu-trigger:hover ul,#bank-library header .sub-menu-trigger:hover ul,#cloud-plugins-library header .sub-menu-trigger:hover ul,#pedalboards-library header .sub-menu-trigger:hover ul,#file-manager-library header .sub-menu-trigger:hover ul{display:block}header#pedalboard-actions button,#bank-library header button,#cloud-plugins-library header button,#pedalboards-library header button,#file-manager-library header button{background-position:8px 50%;background-size:25px;background-repeat:no-repeat;padding:0 12px 0 40px;background-color:transparent;border:none;height:45px;line-height:45px;opacity:.66;color:white;transition:all .33s}header#pedalboard-actions button:hover,#bank-library header button:hover,#cloud-plugins-library header button:hover,#pedalboards-library header button:hover,#file-manager-library header button:hover,header#pedalboard-actions button.selected,#bank-library header button.selected,#cloud-plugins-library header button.selected,#pedalboards-library header button.selected,#file-manager-library header button.selected{opacity:1;background-color:black}header#pedalboard-actions button.js-save,#bank-library header button.js-save,#cloud-plugins-library header button.js-save,#pedalboards-library header button.js-save,#file-manager-library header button.js-save{background-image:url(../img/icons/25/save.png)}header#pedalboard-actions button.js-save-as,#bank-library header button.js-save-as,#cloud-plugins-library header button.js-save-as,#pedalboards-library header button.js-save-as,#file-manager-library header button.js-save-as{background-image:url(../img/icons/25/save.png)}header#pedalboard-actions button.js-preset,#bank-library header button.js-preset,#cloud-plugins-library header button.js-preset,#pedalboards-library header button.js-preset,#file-manager-library header button.js-preset{background-image:url(../img/icons/36/presets.png);background-size:30px 30px}header#pedalboard-actions button#js-preset-enabler:hover,#bank-library header button#js-preset-enabler:hover,#cloud-plugins-library header button#js-preset-enabler:hover,#pedalboards-library header button#js-preset-enabler:hover,#file-manager-library header button#js-preset-enabler:hover,header#pedalboard-actions button#js-preset-enabler.selected,#bank-library header button#js-preset-enabler.selected,#cloud-plugins-library header button#js-preset-enabler.selected,#pedalboards-library header button#js-preset-enabler.selected,#file-manager-library header button#js-preset-enabler.selected{background-color:#883996}header#pedalboard-actions button.js-reset,#bank-library header button.js-reset,#cloud-plugins-library header button.js-reset,#pedalboards-library header button.js-reset,#file-manager-library header button.js-reset{background-image:url(../img/icons/25/new.png)}header#pedalboard-actions button.js-reset:hover,#bank-library header button.js-reset:hover,#cloud-plugins-library header button.js-reset:hover,#pedalboards-library header button.js-reset:hover,#file-manager-library header button.js-reset:hover,header#pedalboard-actions button.js-reset.selected,#bank-library header button.js-reset.selected,#cloud-plugins-library header button.js-reset.selected,#pedalboards-library header button.js-reset.selected,#file-manager-library header button.js-reset.selected{background-color:#883996}header#pedalboard-actions button.js-cv-addressing,#bank-library header button.js-cv-addressing,#cloud-plugins-library header button.js-cv-addressing,#pedalboards-library header button.js-cv-addressing,#file-manager-library header button.js-cv-addressing{background-image:url(../img/icons/25/cv.png)}header#pedalboard-actions button.js-cv-addressing:hover,#bank-library header button.js-cv-addressing:hover,#cloud-plugins-library header button.js-cv-addressing:hover,#pedalboards-library header button.js-cv-addressing:hover,#file-manager-library header button.js-cv-addressing:hover,header#pedalboard-actions button.js-cv-addressing.selected,#bank-library header button.js-cv-addressing.selected,#cloud-plugins-library header button.js-cv-addressing.selected,#pedalboards-library header button.js-cv-addressing.selected,#file-manager-library header button.js-cv-addressing.selected{background-color:#883996}header#pedalboard-actions button.js-cloud,#bank-library header button.js-cloud,#cloud-plugins-library header button.js-cloud,#pedalboards-library header button.js-cloud,#file-manager-library header button.js-cloud{background-image:url(../img/icn-share.png);background-size:32px;background-position:5px center}header#pedalboard-actions h1,#bank-library header h1,#cloud-plugins-library header h1,#pedalboards-library header h1,#file-manager-library header h1{font-weight:2em;text-transform:uppercase;font-weight:normal;color:#999;display:inline-block;font-size:24px;line-height:49px;height:45px;padding:0}header#pedalboard-actions{box-shadow:0 2px 12px black}.tooltip{background:white;bottom:50px;height:auto;padding:7px;position:absolute}.tooltip .text{color:#222;font-weight:bold;line-height:1;text-align:center}.tooltip .arrow{border-bottom:9px solid transparent;border-left:9px solid transparent;border-right:9px solid transparent;border-top:9px solid white;height:0;position:absolute;bottom:-18px;right:14px;width:0}#mod-devices-window,#mod-upgrade{background:white;color:black;font-size:15px;max-height:350px;overflow:auto;position:absolute;right:10px;bottom:50px;z-index:100}#mod-devices-window .progressbar-wrapper,#mod-upgrade .progressbar-wrapper{border:1px solid #444;height:20px;width:330px}#mod-devices-window .progressbar,#mod-upgrade .progressbar{background-color:#88f;height:18px}#mod-devices-window{max-width:600px;min-width:500px}#mod-upgrade{max-width:450px;min-width:350px}#mod-transport-window{bottom:48px;right:310px;top:initial;left:initial;background:rgba(17,17,17,0.9);padding:50px 40px 15px 40px;z-index:999}#mod-transport-window .mod-midi-icon{width:auto !important;padding-left:25px;background-position:5% 45% !important;background-image:url(../img/icons/25/midi.png);background-size:15px;background-repeat:no-repeat}#plugins-library{bottom:45px;height:166px;left:0;position:absolute;right:0;transition:all .33s;transition:opacity .5s;transition:height .5s}#plugins-library.folded{height:38px}#plugins-library.folded #plugins-library-settings-window{bottom:0}#plugins-library.fade{opacity:.2}#plugins-library .fold{cursor:pointer;display:block !important;float:right;height:36px;position:absolute;top:0;right:0;width:36px;background-color:rgba(17,17,17,0.66);background-image:url(../img/icons/25/pinned.png);background-position:center center;background-repeat:no-repeat}#plugins-library .folded{cursor:pointer;display:block;float:right;height:36px;position:absolute;top:0;right:0;width:36px}#plugins-library.auto .fold{background-image:url(../img/icons/25/autohide.png)}#plugins-library .settings{background-color:rgba(17,17,17,0.66);background-image:url(../img/icn-search-white.png);background-position:center center;background-repeat:no-repeat;cursor:pointer;display:block;height:36px;position:absolute;right:37px;top:0;width:36px}#plugins-library #plugins-library-settings-window{background-color:rgba(17,17,17,0.66);bottom:130px;color:#fff;position:absolute;right:74px;transition:bottom .5s}#plugins-library #plugins-library-settings-window h1{padding:0}#plugins-library #plugins-library-settings-window form{padding:4px;border:0;margin:0}#plugins-library #plugins-library-settings-window label{margin-right:10px}#plugins-library #plugins-library-settings-window label.checkbox:last-child{margin-right:0}#plugins-library #plugins-library-settings-window .control-group{position:relative}#plugins-library #plugins-library-settings-window input{width:120px;height:28px;line-height:28px;padding:0 6px;outline:0;border:0}#plugins-library #plugins-library-settings-window .input-clean{position:absolute;top:50%;right:8px;bottom:auto;left:auto;transform:translateY(-50%);margin:0 !important;border-radius:100%}#plugins-library ul{list-style:none;margin:6px 73px 0 0;overflow:hidden;padding:10px 0 0 0;position:relative;width:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}#plugins-library ul li{background:rgba(17,17,17,0.66);color:#bbb;cursor:pointer !important;display:inline-block;float:left;font-size:11px;margin:0 1px 0 0;padding:3px 11px;text-shadow:1px 1px 0 #000;text-transform:uppercase;border-top:solid 3px transparent;transition-property:border-radius,color,margin-top,padding,border-top;transition-duration:.33s}#plugins-library ul li:hover{margin-top:-6px;padding:6px 11px;color:white}#plugins-library ul li.selected{color:white;font-weight:bold;margin-top:-6px;padding:6px 11px;border-top:solid 3px #883996}#plugins-library.scrolling .plugins-wrapper{transition:all .1s !important}#plugins-library #plugins-list{background:rgba(17,17,17,0.66);height:140px;overflow:hidden;position:relative;width:100%;z-index:1}#plugins-library #plugins-list .nav{background:url(../img/nav-left.png) 50% 50% no-repeat;cursor:pointer;position:absolute;top:0;width:50px;height:100%;opacity:.66}#plugins-library #plugins-list .nav:hover{opacity:1}#plugins-library #plugins-list .nav-left{left:0}#plugins-library #plugins-list .nav-right{background:url(../img/nav-right.png) 50% 50% no-repeat;right:0}#plugins-library #plugins-list #plugins-results{bottom:0;left:50px;position:absolute;overflow:hidden;right:50px;top:0}#plugins-library #plugins-list .plugins-wrapper{position:absolute;display:inline-block;white-space:nowrap;transition:all .5s;top:120px;left:0;opacity:0}#plugins-library #plugins-list .plugins-wrapper.selected{top:0;opacity:1}#plugins-library #plugins-list .plugins-wrapper .plugin{color:#fff;font-size:11px;font-weight:bold;height:108px;padding:14px 12px 10px;position:relative;text-align:center;display:inline-block}#plugins-library #plugins-list .plugins-wrapper .plugin .thumb{height:67px;cursor:pointer !important;position:relative;display:flex;flex-direction:column;justify-content:center}#plugins-library #plugins-list .plugins-wrapper .plugin .thumb:active{cursor:-webkit-grabbing !important;cursor:grabbing !important}#plugins-library #plugins-list .plugins-wrapper .plugin .thumb img{height:auto;margin:0 auto 3px;width:auto;pointer-events:none;max-width:256px;max-height:64px}#plugins-library #plugins-list .plugins-wrapper .plugin .status{display:block;height:16px;margin:0 auto;width:16px}#plugins-library #plugins-list .plugins-wrapper .plugin .status.demo{display:inline-block;width:auto;height:auto;margin:0}#plugins-library #plugins-list .plugins-wrapper .plugin .status.installed{background-image:url(../img/icn-installed.png);background-position:center center;background-repeat:no-repeat}#plugins-library #plugins-list .plugins-wrapper .plugin .status.outdated{background-image:url(../img/icn-outdated.png);background-position:center center;background-repeat:no-repeat}#plugins-library #plugins-list .plugins-wrapper .plugin .status.blocked{background-image:url(../img/icn-blocked.png);background-position:center center;background-repeat:no-repeat}#plugins-library #plugins-list .plugins-wrapper .plugin .author{display:block;line-height:1.5}#plugins-library #plugins-list .plugins-wrapper .plugin .title{display:block;line-height:1.25}#plugins-library #plugins-list .plugins-wrapper .plugin .rating{background-image:url(../img/icn-rating.png);background-position:center top;background-repeat:no-repeat;display:block;height:16px;width:132px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.one{background-position:center -15px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.two{background-position:center -30px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.three{background-position:center -45px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.four{background-position:center -60px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.five{background-position:center -75px}#plugins-library #plugins-list .plugins-wrapper .plugin .downloads{display:block}#plugins-library #plugins-list .plugins-wrapper .plugin .demo-container{position:absolute;left:0;bottom:3px;right:0;text-align:center}.plugin-info{background-color:rgba(20,20,20,0.9);bottom:0;color:#ccc;left:0;overflow-y:scroll;position:fixed;right:0;top:0;z-index:40}.plugin-info .box{margin-top:3em;margin-bottom:3em;background-color:#111;padding:15px;border:2px solid #333}.plugin-info .box .row-fluid{position:relative}.plugin-info .close{color:#fff;font-size:12px;font-weight:bold;opacity:1;position:absolute;right:10px;text-shadow:none;text-transform:uppercase;top:10px}.plugin-info .screenshot{padding:18px;min-width:3em;min-height:9em;display:inline-block;z-index:0}.plugin-info .screenshot div.beta{background-color:#ff2927;font-family:"cooper hewitt",sans-serif;font-weight:700;padding:5px 10px;color:white;position:absolute;top:18px;left:30px;font-size:16px}.plugin-info figure{position:relative;min-height:1px;padding-right:15px;padding-left:15px}@media (min-width:992px){.plugin-info figure{float:left;width:33.33333333%}}.plugin-info figure:hover{z-index:10 !important}.plugin-info .plugin-description{position:relative;min-height:1px;padding-right:15px;padding-left:15px;height:100%;display:block;position:a;padding-left:45px;background:-moz-linear-gradient(left, rgba(17,17,17,0.1) 1em, #111 2em);background:-webkit-gradient(linear, left top, right top, color-stop(1em, rgba(17,17,17,0.1)), color-stop(2em, #111));background:-webkit-linear-gradient(left, rgba(17,17,17,0.1) 1em, #111 2em);background:-o-linear-gradient(left, rgba(17,17,17,0.1) 1em, #111 2em);background:-ms-linear-gradient(left, rgba(17,17,17,0.1) 1em, #111 2em);background:linear-gradient(to right, rgba(17,17,17,0.1) 1em, #111 2em);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffffff', endColorstr='#ffffff', GradientType=1)}@media (min-width:992px){.plugin-info .plugin-description{float:left;width:58.33333333%}}.plugin-info .plugin-description>p{font-size:16px;margin-bottom:27px;white-space:pre-wrap}.plugin-info header{padding:18px 0 0}.plugin-info header h1{display:inline-block;vertical-align:middle;color:#fff;font-size:27px;font-weight:bold;line-height:1;margin:0;padding:0}.plugin-info header h1 span{font-size:16px;line-height:1;margin:0;padding:0}.plugin-info .version{color:#ccc;font-size:12px;font-weight:bold;text-transform:uppercase}.plugin-info .version span.bold{color:#fff;font-size:14px}.plugin-info .version button{font-weight:bold;margin-top:6px;text-transform:uppercase}.plugin-info .rating{background:url(../img/icn-rating.png) no-repeat -12px top;display:inline-block;height:14px;width:84px}.plugin-info .rating.one{background:url(../img/icn-rating.png) no-repeat -12px -15px}.plugin-info .rating.two{background:url(../img/icn-rating.png) no-repeat -12px -30px}.plugin-info .rating.three{background:url(../img/icn-rating.png) no-repeat -12px -45px}.plugin-info .rating.four{background:url(../img/icn-rating.png) no-repeat -12px -60px}.plugin-info .rating.five{background:url(../img/icn-rating.png) no-repeat -12px -75px}.plugin-info .rating-label{color:#fff;font-size:12px;font-weight:bold;text-transform:uppercase}.plugin-info .my-rate{width:15px;height:15px;margin:0;padding:0;float:left;cursor:pointer}.plugin-info .plugin-specs{text-align:center;width:100%;border-color:#333}.plugin-info .plugin-specs th{background:#333;color:#fff;border-color:#333;text-align:center}.plugin-info .plugin-specs td{border-color:#333}.plugin-info .plugin-specs td:first-child{width:40%}.plugin-info .plugin-specs td:not(first-child){width:20%}.plugin-info .comments-reviews{border-top:1px solid #333;margin-top:10px;padding-top:10px}.plugin-info .comments-reviews h1{color:#fff;font-size:21px;line-height:2;margin:0;padding:0}.plugin-info .reviews{border-bottom:1px solid #333;margin:9px 0 18px 0;padding:9px 0 18px 0}.plugin-info article.review{margin:18px 0}.plugin-info article.review h1{color:#fff;font-size:14px}.plugin-info article.review h1 .rating{margin-left:6px}.plugin-info article.review h1 .rating-label{color:#ccc;display:inline;font-size:12px;font-weight:normal;margin-left:6px}.plugin-info article.review .bar{background:#333;display:block;height:12px;margin:2px 6px 0;position:relative;width:100px}.plugin-info article.review .progress-bar{background:#444;bottom:0;display:block;height:10px;left:0;margin:1px;position:absolute;right:0;top:0;width:50%}.plugin-info article.review .rating-number{display:block;font-size:12px;margin-top:-1px}.plugin-info article.comment{border-top:1px solid #333;margin:18px 0 0;padding:18px 0 0}.plugin-info article.comment h1{color:#fff;font-size:16px;line-height:1}.plugin-info article.comment h1 .date{color:#ccc;font-size:12px;font-weight:normal;margin-left:6px}.plugin-info .comments{margin:18px 0;padding:18px 0}.plugin-info .comments button{margin-top:18px}.plugin-info form.comment-form{border-top:1px solid #333;margin:18px 0;padding:18px 0}.plugin-info form.comment-form label{font-size:12px;font-weight:bold;text-transform:uppercase}.plugin-info form.comment-form textarea{height:180px}.plugin-info .favorite-button{display:inline-block;vertical-align:middle;width:30px;height:30px;background:url(../img/icons/25/star-border.png) no-repeat center center;cursor:pointer;opacity:.5}.plugin-info .favorite-button:hover{opacity:.8}.plugin-info .favorite-button.favorite{opacity:1;background:url(../img/icons/25/star.png) no-repeat center center}.plugin-info .favorite-button.favorite:hover{opacity:1}.plugin-info .online-button-href{display:inline-block;margin-bottom:10px}.plugin-info .status.demo{display:inline-block;padding:4px 8px;margin:2px 0;font-size:.8em}.clearfix:after,.container:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after{content:"\0020";display:block;height:0;clear:both;visibility:hidden;overflow:hidden}.clearfix,.container,.dl-horizontal dd,.container,.container-fluid,.row,.form-horizontal .form-group,.btn-toolbar,.btn-group-vertical>.btn-group,.nav,.navbar,.navbar-header,.navbar-collapse,.pager,.panel-body{display:block}.clear{clear:both}.blend{opacity:.25}.box{padding:10px}.alignleft{float:left}.alignright{float:right}.textcenter{text-align:center}.textright{text-align:right}.bottom{margin-bottom:0 !important;padding-bottom:0 !important}.left{margin-left:0 !important;padding-left:0 !important}.right{margin-right:0 !important;padding-right:0 !important}.top{margin-top:0 !important;padding-top:0 !important}.nobullets{list-style:none}.mod-hidden{display:none}.mod-init-hidden{opacity:0}::selection{background:transparent}.mod-box{background:white;left:50%;margin:-150px -168px;padding:18px;position:absolute;top:50%;width:300px}.mod-box:not(#midi-ports-box) label{display:inline-block;float:left;line-height:2;width:96px}.mod-box .mod-controls{float:left}.mod-box input[type="text"]{width:190px}.mod-box input[type="password"]{width:190px}.blocker{background:rgba(17,17,17,0.9);color:white;display:block;position:fixed;width:100% !important;height:100% !important;top:0;left:0;text-align:center;z-index:99999}.blocker p{position:absolute;top:50%;width:100%;margin-top:56px;font-size:24px}.blocker.screen-disconnected{background:rgba(17,17,17,0.9) url(../img/icons/blocked.svg) 50% 50% no-repeat;background-size:100px 100px}.blocker.screen-disconnected .button{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);margin-top:120px}.plugin-wait{background:url(../img/loading-effect.gif) no-repeat center center;border:1px solid #ccc}.button{background:#883996;color:white;display:inline-block;cursor:pointer;line-height:25px;padding:5px 15px;transition:all .33s;border:none}.button:hover{background-color:#f29446;box-shadow:0 1px 5px black}.button.icon{padding-left:40px;background-position:10px 50%;background-repeat:no-repeat;background-size:25px}.drag-handle{bottom:0;cursor:-webkit-grabbing !important;cursor:grabbing !important;left:0;position:absolute;right:0;top:0;z-index:20}.demo-plugin{background:url("/img/trial.svg") no-repeat center center;background-size:contain;opacity:.4;z-index:11 !important}.demo-mask{width:100%;height:100%;left:0;right:0;top:0;bottom:0;position:absolute}.demo-plugin-light{opacity:.1 !important}.actions{z-index:20}@keyframes flashlight{0%{box-shadow:0 0 0 transparent}50%{box-shadow:0 0 10px white}100%{box-shadow:0 0 0 transparent}}.alert{background:rgba(17,17,17,0.9);border-width:0 0 0 6px;border-color:#883996;color:white;box-shadow:0 3px 10px black;margin-top:5px !important}.alert .js-close{color:white}.alert-warning{border-color:#f29446}.alert-danger{border-color:#c00}#go-back{width:200px;text-align:left;margin:20px 0 0 0;background:none;border:none;font-size:15px;padding:5px;color:#fff !important}#go-back span{font-family:fontello !important}#go-back span:before{content:'\e804'}#go-back:hover{text-decoration:none}.unstable-warning{float:left;background-color:#ffebb1;padding:5px;border-radius:10px} \ No newline at end of file +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,*:before,*:after{color:#000 !important;text-shadow:none !important;background:transparent !important;box-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#883996;text-decoration:none}a:hover,a:focus{color:#56245f;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:0}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:0;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#999}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{padding:.2em;background-color:#f29446}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#999}.text-primary{color:#883996}a.text-primary:hover,a.text-primary:focus{color:#662b71}.text-success{color:#3c763d}a.text-success:hover,a.text-success:focus{color:#2b542c}.text-info{color:#3a633d}a.text-info:hover,a.text-info:focus{color:#274329}.text-warning{color:#fff}a.text-warning:hover,a.text-warning:focus{color:#e6e6e6}.text-danger{color:#a94442}a.text-danger:hover,a.text-danger:focus{color:#843534}.bg-primary{color:#fff;background-color:#883996}a.bg-primary:hover,a.bg-primary:focus{background-color:#662b71}.bg-success{background-color:#dff0d8}a.bg-success:hover,a.bg-success:focus{background-color:#c1e2b3}.bg-info{background-color:#a6ecb1}a.bg-info:hover,a.bg-info:focus{background-color:#7ce38c}.bg-warning{background-color:#f29446}a.bg-warning:hover,a.bg-warning:focus{background-color:#ef7816}.bg-danger{background-color:#f2dede}a.bg-danger:hover,a.bg-danger:focus{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#999}blockquote footer:before,blockquote small:before,blockquote .small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:""}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:"\00A0 \2014"}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:0}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:0;box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:0}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:100%}}@media (min-width:992px){.container{width:970px}}@media (min-width:1240px){.container{width:1270px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*="col-"]{padding-right:0;padding-left:0}.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1240px){.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}table col[class*="col-"]{position:static;display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#999;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#a6ecb1}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#91e89f}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#f29446}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#f0862e}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none;appearance:none}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);box-shadow:inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{line-height:34px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:30px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:46px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-top:4px \9;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:0}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:0}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm textarea.form-control,.form-group-sm select[multiple].form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:0}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:0}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg textarea.form-control,.form-group-lg select[multiple].form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.33}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback,.input-group-lg+.form-control-feedback,.form-group-lg .form-control+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback,.input-group-sm+.form-control-feedback,.form-group-sm .form-control+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#fff}.has-warning .form-control{border-color:#fff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-warning .input-group-addon{color:#fff;background-color:#f29446;border-color:#fff}.has-warning .form-control-feedback{color:#fff}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;white-space:nowrap;vertical-align:middle;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);opacity:.65;-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:focus,.btn-default.focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#883996;border-color:#773284}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#662b71;border-color:#230f27}.btn-primary:hover{color:#fff;background-color:#662b71;border-color:#4f2157}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#662b71;background-image:none;border-color:#4f2157}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#4f2157;border-color:#230f27}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#883996;border-color:#773284}.btn-primary .badge{color:#883996;background-color:#fff}.btn-success{color:#fff;background-color:#883996;border-color:#773284}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#662b71;border-color:#230f27}.btn-success:hover{color:#fff;background-color:#662b71;border-color:#4f2157}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#662b71;background-image:none;border-color:#4f2157}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#fff;background-color:#4f2157;border-color:#230f27}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#883996;border-color:#773284}.btn-success .badge{color:#883996;background-color:#fff}.btn-info{color:#fff;background-color:#883996;border-color:#773284}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#662b71;border-color:#230f27}.btn-info:hover{color:#fff;background-color:#662b71;border-color:#4f2157}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#662b71;background-image:none;border-color:#4f2157}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#fff;background-color:#4f2157;border-color:#230f27}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#883996;border-color:#773284}.btn-info .badge{color:#883996;background-color:#fff}.btn-warning{color:#fff;background-color:#f29446;border-color:#f0862e}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#ef7816;border-color:#95490a}.btn-warning:hover{color:#fff;background-color:#ef7816;border-color:#d3680f}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ef7816;background-image:none;border-color:#d3680f}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#fff;background-color:#d3680f;border-color:#95490a}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#f29446;border-color:#f0862e}.btn-warning .badge{color:#f29446;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;background-image:none;border-color:#ac2925}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#883996;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#56245f;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:0}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:0}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:0}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height, visibility;transition-property:height, visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid \9;border-right:4px solid transparent;border-left:4px solid transparent}.dropup,.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:0;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#883996;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#999;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid \9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle="buttons"]>.btn input[type="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:0}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:0}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:0}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:0}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:0}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#883996}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:0 0 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:0 0 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:0}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#883996}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:0 0 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:0}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:0}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-right:-15px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left !important}.navbar-right{float:right !important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#999}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:0}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:0}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#883996;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{z-index:2;color:#56245f;background-color:#eee;border-color:#ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:3;color:#fff;cursor:default;background-color:#883996;border-color:#883996}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.33}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:0;border-bottom-left-radius:0}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:0;border-bottom-left-radius:0}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#883996}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#662b71}.label-success{background-color:#883996}.label-success[href]:hover,.label-success[href]:focus{background-color:#662b71}.label-info{background-color:#883996}.label-info[href]:hover,.label-info[href]:focus{background-color:#662b71}.label-warning{background-color:#f29446}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ef7816}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#883996;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:0}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:0;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#883996}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:0}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#3a633d;background-color:#a6ecb1;border-color:#89e689}.alert-info hr{border-top-color:#75e174}.alert-info .alert-link{color:#274329}.alert-warning{color:#fff;background-color:#f29446;border-color:#f0662e}.alert-warning hr{border-top-color:#ef5416}.alert-warning .alert-link{color:#e6e6e6}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:0;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#883996;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#883996}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#883996}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#f29446}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{color:#999;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#999}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#883996;border-color:#883996}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#ddb7e4}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#3a633d;background-color:#a6ecb1}a.list-group-item-info,button.list-group-item-info{color:#3a633d}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#3a633d;background-color:#91e89f}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#3a633d;border-color:#3a633d}.list-group-item-warning{color:#fff;background-color:#f29446}a.list-group-item-warning,button.list-group-item-warning{color:#fff}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#fff;background-color:#f0862e}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#fff;border-color:#fff}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:-1px;border-top-right-radius:-1px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a,.panel-title>small,.panel-title>.small,.panel-title>small>a,.panel-title>.small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:-1px;border-top-right-radius:-1px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-right:15px;padding-left:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-left-radius:-1px;border-top-right-radius:-1px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:-1px;border-top-right-radius:-1px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:-1px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:-1px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:-1px;border-bottom-left-radius:-1px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:-1px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:-1px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:0}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#444}.panel-default>.panel-heading{color:#999;background-color:#444;border-color:#444}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#444}.panel-default>.panel-heading .badge{color:#444;background-color:#999}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#444}.panel-primary{border-color:#883996}.panel-primary>.panel-heading{color:#fff;background-color:#883996;border-color:#883996}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#883996}.panel-primary>.panel-heading .badge{color:#883996;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#883996}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#89e689}.panel-info>.panel-heading{color:#3a633d;background-color:#a6ecb1;border-color:#89e689}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#89e689}.panel-info>.panel-heading .badge{color:#a6ecb1;background-color:#3a633d}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#89e689}.panel-warning{border-color:#f0662e}.panel-warning>.panel-heading{color:#fff;background-color:#f29446;border-color:#f0662e}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#f0662e}.panel-warning>.panel-heading .badge{color:#f29446;background-color:#fff}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#f0662e}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:0}.well-sm{padding:9px;border-radius:0}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none;appearance:none}.tooltip{position:absolute;z-index:1030;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:12px;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:0}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.clearfix,.dl-horizontal dd,.container,.container-fluid,.row,.form-horizontal .form-group,.btn-toolbar,.btn-group-vertical>.btn-group,.nav,.navbar,.navbar-header,.navbar-collapse,.pager,.panel-body{display:block}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after{display:table;content:" "}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (max-width:767px){.visible-xs-block{display:block !important}}@media (max-width:767px){.visible-xs-inline{display:inline !important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table !important}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block !important}}@media (min-width:992px) and (max-width:1239px){.visible-md{display:block !important}table.visible-md{display:table !important}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:992px) and (max-width:1239px){.visible-md-block{display:block !important}}@media (min-width:992px) and (max-width:1239px){.visible-md-inline{display:inline !important}}@media (min-width:992px) and (max-width:1239px){.visible-md-inline-block{display:inline-block !important}}@media (min-width:1240px){.visible-lg{display:block !important}table.visible-lg{display:table !important}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (min-width:1240px){.visible-lg-block{display:block !important}}@media (min-width:1240px){.visible-lg-inline{display:inline !important}}@media (min-width:1240px){.visible-lg-inline-block{display:inline-block !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1239px){.hidden-md{display:none !important}}@media (min-width:1240px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table !important}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}}#cloud-plugins-library{background:#2c2c2c;bottom:45px;left:0;overflow:auto;position:fixed;right:0;top:0;z-index:2}#cloud-plugins-library h1{color:#fff;display:inline-block;font-size:24px;line-height:35px;margin:0 0 18px 0;padding:0}#cloud-plugins-library header .help{background-image:url(../img/icn-help-16x16.png);background-position:center center;background-repeat:no-repeat;display:inline-block;height:21px;width:21px}#cloud-plugins-library header .close{opacity:1;position:absolute;right:10px;top:16px}#cloud-plugins-library .filter{bottom:45px;left:0;position:fixed;top:45px;right:0;width:240px;overflow:auto}#cloud-plugins-library .filter h1{font-weight:2em;text-transform:uppercase;font-weight:bold;margin:0 0 0 10px}#cloud-plugins-library .filter h2{color:white;font-size:15px;text-transform:uppercase;font-weight:bold;margin-top:1em;margin-left:10px}#cloud-plugins-library .filter .control-group .switch{margin:5px 0 5px 15px;position:relative;width:18rem;height:2.5rem;font-size:0}#cloud-plugins-library .filter .control-group .switch input{position:absolute;top:0;z-index:2;opacity:0;cursor:pointer;height:2.5rem;width:5rem;left:5.5rem;margin:0}#cloud-plugins-library .filter .control-group .switch input:checked{z-index:1}#cloud-plugins-library .filter .control-group .switch input:checked+label{opacity:1;cursor:default}#cloud-plugins-library .filter .control-group .switch input:checked~.toggle-outside .toggle-inside{left:.25rem;background:#883996}#cloud-plugins-library .filter .control-group .switch input:not(:checked)+label:hover{opacity:.5}#cloud-plugins-library .filter .control-group .switch input~input:checked~.toggle-outside .toggle-inside{left:2.65rem;background:#4a4a4a}#cloud-plugins-library .filter .control-group .switch label{color:#fff;opacity:.33;transition:opacity .25s ease;cursor:pointer;font-size:14px;line-height:3rem;display:inline-block;width:5rem;height:100%;margin:0;text-align:center}#cloud-plugins-library .filter .control-group .switch label:first-of-type{text-align:left}#cloud-plugins-library .filter .control-group .switch label:last-of-type{margin-left:5.5rem}#cloud-plugins-library .filter .control-group .switch .toggle-outside{height:100%;border-radius:2rem;padding:.25rem;overflow:hidden;transition:.25s ease all;background:#fff;opacity:.8;position:absolute;width:5rem;left:5.5rem}#cloud-plugins-library .filter .control-group .switch .toggle-inside{border-radius:5rem;background:#4a4a4a;position:absolute;transition:.25s ease all;height:2rem;width:2rem}#cloud-plugins-library .filter .control-group select{width:50px}#cloud-plugins-library .filter .control-group input[type='checkbox']{display:none}#cloud-plugins-library .filter .control-group input[type='checkbox']+label{cursor:pointer;background:#222;color:#fff;display:block;height:32px;line-height:32px;width:100%;margin-bottom:1px;padding-left:15px;border:0 solid #f29446}#cloud-plugins-library .filter .control-group input[type='checkbox']+label:hover{background:#000}#cloud-plugins-library .filter .control-group input[type='checkbox']:checked+label{background:#f29446}#cloud-plugins-library .filter .control-group input[type='checkbox']:checked+label:hover{background:#600e6f}#cloud-plugins-library .filter>.form-group .input-clean{margin-right:8px !important}#cloud-plugins-library .filter .categories{width:100%;margin:0 0 15px 0}#cloud-plugins-library .filter .categories li{display:block;background:#222;color:#888;cursor:pointer;font-weight:bold;margin:0 0 1px 0;padding:0 15px;position:relative;transition:all .33s;line-height:32px;height:32px}#cloud-plugins-library .filter .categories li span{float:right}#cloud-plugins-library .filter .categories li.selected{background:#883996;color:#fff}#cloud-plugins-library .filter .categories li:hover{background:#000}#cloud-plugins-library .filter .categories li.selected:hover{background:#600e6f}.cloud-plugins{left:240px;position:fixed;right:0;top:45px;bottom:45px;background:#111 url(../img/watermark.png) 100% 100% no-repeat;overflow-y:auto}.cloud-plugins .plugins-wrapper{width:100%}.cloud-plugins .cloud-plugin{color:#fff;float:left;width:100%;position:relative;min-height:1px;padding-right:15px;padding-left:15px;height:120px;border:5px solid #000;padding:0 !important;background:rgba(255,255,255,0.07);transition:all .33s;cursor:pointer}@media (min-width:768px){.cloud-plugins .cloud-plugin{float:left;width:50%}}@media (min-width:992px){.cloud-plugins .cloud-plugin{float:left;width:50%}}@media (min-width:1240px){.cloud-plugins .cloud-plugin{float:left;width:33.33333333%}}.cloud-plugins .cloud-plugin:hover{background-color:black}.cloud-plugins .cloud-plugin:hover .description{background:rgba(0,0,0,0.8)}.cloud-plugins .cloud-plugin:hover .description hr{border-color:#f29446}.cloud-plugins .cloud-plugin:hover figure{top:-5px}.cloud-plugins .cloud-plugin:hover div.demo-plugin{opacity:0}.cloud-plugins .cloud-plugin:hover span.price{position:absolute;top:25px;margin-left:98px;padding:3px 0;transform:rotate(0deg) scale(1.3);width:80px}.cloud-plugins .cloud-plugin:hover span.licensed{position:absolute;top:25px;margin-left:98px;padding:3px 0;transform:rotate(0deg) scale(1.3);width:113px;text-align:center}.cloud-plugins .cloud-plugin:hover span.coming-soon{position:absolute;top:25px;margin-left:98px;padding:3px 0;transform:rotate(0deg) scale(1.3);width:113px;text-align:center}.cloud-plugins .cloud-plugin:hover span.price-container{margin-left:170px;width:200px}.cloud-plugins .cloud-plugin figure{height:100px;position:absolute;top:10px;left:20px;margin:0;transition:all .33s linear;display:flex;flex-direction:column;justify-content:center}.cloud-plugins .cloud-plugin figure img{height:auto;margin:0 auto 3px;width:auto;pointer-events:none;max-width:256px;max-height:64px}.cloud-plugins .cloud-plugin span.price{transition:all .33s linear}.cloud-plugins .cloud-plugin span.price-container{transition:all .33s linear}.cloud-plugins .cloud-plugin .cloud-plugin-border{border:1px solid #333;width:100%;height:100%}.cloud-plugins .cloud-plugin .description{transition:all .33s;display:block;line-height:1.5;overflow:hidden;position:absolute;top:1px;right:1px;bottom:1px;left:80px;background:rgba(33,33,33,0.66)}.cloud-plugins .cloud-plugin .description .no_description{color:rgba(255,255,255,0.25)}.cloud-plugins .cloud-plugin .description hr{transition:all .33s;border:none;border-bottom:1px solid #883996;margin:0;position:absolute;top:50%;left:0;width:calc(100% - 5px)}.cloud-plugins .cloud-plugin .description .title{display:block;text-transform:uppercase;font-weight:bold;overflow:hidden;padding:8px 10px 1px 10px}.cloud-plugins .cloud-plugin .description .author{display:block;padding:1px 10px}.cloud-plugins .cloud-plugin .description p{position:absolute;left:10px;right:10px;top:75%;transform:translateY(-50%);height:2.4em;line-height:1.25em;overflow:hidden}.cloud-plugins .cloud-plugin .actions{font-size:11px;font-weight:bold;text-transform:uppercase}.cloud-plugins .cloud-plugin .actions>span{cursor:pointer}.cloud-plugins .cloud-plugin .actions>span:hover{text-decoration:underline}.cloud-plugins .cloud-plugin .actions span.duplicate{margin:0 6px}.cloud-plugins h2{margin-left:23px;font-size:20px;color:#ccc}.cloud-plugins h3{color:#ccc}.cloud-plugins .featured-plugins{position:relative;height:360px;width:100%;display:block}.cloud-plugins .featured-plugins.double,.cloud-plugins .featured-plugins.single{height:490px !important}.cloud-plugins .featured-plugins.double .box,.cloud-plugins .featured-plugins.single .box{transition:all 0s !important}.cloud-plugins .featured-plugins.double .carousel,.cloud-plugins .featured-plugins.single .carousel{height:485px !important}.cloud-plugins .featured-plugins.double .carousel .slick-list,.cloud-plugins .featured-plugins.single .carousel .slick-list{height:485px !important}.cloud-plugins .featured-plugins.double .box{margin:30px 80px !important}.cloud-plugins .featured-plugins.single .box{margin:30px 30px !important}.cloud-plugins .featured-plugins .carousel{position:absolute;top:0;bottom:0;right:90px;left:90px;height:380px !important}.cloud-plugins .featured-plugins .carousel .slick-list{height:380px !important}.cloud-plugins .featured-plugins .carousel .featured{cursor:pointer;margin-top:40px}.cloud-plugins .featured-plugins .carousel .featured .box{padding:4px;border:1px solid #737373;height:300px;opacity:.4;margin:0 24px}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box{border:1px solid #444;text-align:center;height:290px}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .price-container{position:absolute;top:0;left:0;width:90px;height:90px;overflow:hidden;color:#fff}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .price{background-color:#883996;border:1px solid #883996;position:absolute;top:9px;left:-30px;padding:3px 0;transform:rotate(-45deg);min-width:108px;text-align:center}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .price:before{content:"€ "}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .licensed{background-color:#883996;border:1px solid #883996;position:absolute;top:16px;left:-30px;padding:3px 27px;font-size:12px;transform:rotate(-45deg)}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .licensed:before{content:"PURCHASED"}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box h2{color:#c3c3c3;text-align:center}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .img-container{height:113px;width:100%}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box .img-container img{vertical-align:middle;transition:all 500ms ease;width:auto;height:auto;margin:auto;display:block;max-height:113px;padding:0 20px;max-width:100%}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box p{color:#e0e0e0;padding:5px 5px 0 5px;height:59px}.cloud-plugins .featured-plugins .carousel .featured .box .inner-box button{background-color:transparent;color:#ff3838;border:1px solid #ff3838;text-transform:uppercase;padding:8px 20px}.cloud-plugins .featured-plugins .carousel .featured.slick-center{height:390px}.cloud-plugins .featured-plugins .carousel .featured.slick-center .box{opacity:1;transform:scale(1.2) translate(0, 0);transition:all 500ms ease}.cloud-plugins .featured-plugins .carousel .slick-arrow{width:60px;height:60px;top:200px}.cloud-plugins .featured-plugins .carousel .slick-arrow:before{font-size:60px}.cloud-plugins .featured-plugins .carousel .slick-prev{left:-70px !important}.cloud-plugins .featured-plugins .carousel .slick-next{right:-70px !important}.plugin-container .status-container{position:absolute;top:0;left:0;right:2px;text-align:right;padding-right:2px}.plugin-container .status{padding:1px 4px;text-align:center;text-transform:uppercase;font-weight:bold;font-size:9px;margin-left:1px}.plugin-container .status.installed{border:1px solid #883996;background-color:#883996}.plugin-container .status.installed:after{content:"installed"}.plugin-container .status.outdated{border:1px solid #f29446;background-color:#f29446;color:#212121}.plugin-container .status.outdated:after{content:"new version"}.plugin-container .price-container{position:absolute;top:0;left:0;width:90px;height:90px;overflow:hidden}.plugin-container .price{background-color:#883996;border:1px solid #883996;position:absolute;top:9px;left:-25px;padding:3px 0;transform:rotate(-45deg);min-width:108px;text-align:center}.plugin-container .price:before{content:"€ "}.plugin-container .licensed{background-color:#883996;border:1px solid #883996;position:absolute;top:21px;left:-30px;padding:3px 27px;font-size:14px;transform:rotate(-45deg)}.plugin-container .licensed:before{content:"PURCHASED"}.plugin-container .coming-soon{background-color:#883996;border:1px solid #883996;position:absolute;top:26px;left:-35px;padding:3px 27px;font-size:12px;transform:rotate(-45deg);width:135px}.plugin-container .coming-soon:before{content:"TRIAL ONLY"}.plugin-container .status.buildEnvironment{border:1px solid #ff2927;background-color:#ff2927}.plugin-container .status.buildEnvironment:after{content:"local"}.plugin-container .status.buildEnvironment.labs:after{content:"labs"}.plugin-container .status.buildEnvironment.dev:after{content:"dev"}.plugin-container .status.buildEnvironment.prod{display:none}.plugin-container .status.buildEnvironment.prod:after{content:""}.plugin-container .demo{border:1px solid #ff2927;background-color:#ff2927}.plugin-container .demo:after{content:"demo"}.plugin-container .description-price-container{position:absolute;top:-17px;left:-17px;overflow:hidden;height:200px}.plugin-container .description-price-container .description-price{background-color:#883996;border:1px solid #883996;position:relative;top:34px;left:-57px;padding:3px 0;transform:rotate(-45deg);min-width:250px;text-align:center;font-size:34px;z-index:11}.plugin-container .description-price-container .description-price:before{content:"€ "}.plugin-container .description-price-container .description-licensed{background-color:#883996;border:1px solid #883996;position:relative;top:50px;left:-60px;padding:3px 0;transform:rotate(-45deg);min-width:280px;text-align:center;font-size:34px;z-index:1}.plugin-container .description-price-container .description-licensed:before{content:"PURCHASED"}.plugin-container .description-price-container .description-coming-soon{background-color:#883996;border:1px solid #883996;position:relative;top:50px;left:-70px;padding:3px 0;transform:rotate(-45deg);min-width:280px;text-align:center;font-size:26px;z-index:1}.plugin-container .description-price-container .description-coming-soon:before{content:"TRIAL ONLY"}.buy-button{margin-top:-20px;position:relative}.buy-button iframe{position:absolute;top:-17px;left:0;min-width:115px;z-index:1}.shopify-fake-button{position:absolute;top:3px;left:0;z-index:0;background-color:#c224b0;font-family:Arial;font-size:12px;opacity:.25;min-width:115px;min-height:34px;padding:9px 15px;text-align:center}#bank-library{bottom:45px;left:0;overflow-x:hidden;overflow-y:auto;position:absolute;right:0;top:0;z-index:2}#bank-library .ui-draggable-dragging{cursor:webkit-dragging !important}#bank-library .mod-banks-drag-item{background:#f29446;color:white;font-weight:bold;z-index:10000;box-shadow:0 5px 15px black;opacity:.5}#bank-library .mod-banks-drag-item .img{display:block;height:auto;margin:0 auto;text-align:center;width:100%;background:url(../img/background.jpg);background-size:auto 100%;background-position:center center;box-shadow:inset 0 3px 6px black;border:1px solid #000}#bank-library .mod-banks-drag-item .title{display:block;line-height:32px;font-weight:bold;overflow:hidden;position:relative;padding-left:5px}#bank-library span.move{background-image:url(../img/move.png);background-position:left center;background-repeat:no-repeat;display:block;float:left;height:32px;left:0;cursor:move !important;margin:-1px 6px 0 0;width:24px}#bank-library span.remove{cursor:pointer;float:right;width:32px;height:32px;display:block;background-image:url(../img/icons/25/delete.png);background-repeat:no-repeat;background-position:50% 50%;transition:background-color .33s}#bank-library span.remove:hover{background-color:#f29446}#bank-library span.title{display:block;float:left;height:32px;overflow:hidden;max-width:70%;white-space:nowrap}#bank-library input::selection{background:#883996 !important;color:white}#bank-library .icon-add{width:1em;height:1em;display:inline-block;font-size:inherit;vertical-align:baseline}#bank-library .icon-add i{position:absolute;width:1em;margin-top:.3em;border-top:.4em solid #888}#bank-library .icon-add b{position:absolute;height:1em;margin-left:.3em;border-left:.4em solid #888}#bank-library #bank-list{left:0;list-style:none;margin:0;padding:0;position:fixed;bottom:45px;overflow-y:auto;top:45px;width:20%;background:#2c2c2c}#bank-library #bank-list>div.add-bank{color:#fff;cursor:pointer;padding-left:5px;line-height:32px;text-transform:uppercase;transition:all .33s}#bank-library #bank-list>div.add-bank:hover{background:#f29446}#bank-library #bank-list>div.add-bank .icon-add{display:inline-block;width:24px;margin-right:6px;padding-left:5px;box-sizing:border-box}#bank-library #bank-list>div.selected span.move{background-position:right center}#bank-library #bank-list>div.selected span.remove{background-position:right center}#bank-library #bank-list .edit-bank{color:#555;width:100% !important;height:30px;margin:1px;padding-left:.5em;background-color:#fff;background-image:none;border:0 solid #ccc;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out}#bank-library #bank-list div{background:#222;color:#888;cursor:pointer;font-weight:bold;margin:0 0 1px 0;content:\002B;position:relative;transition:all .33s}#bank-library #bank-list div.selected{background:#883996;color:#fff}#bank-library #bank-list div:hover{background:#000}#bank-library #bank-list div.selected:hover{background:#600e6f}#bank-library #bank-list .bank-item{display:block;background:#222;color:#888;line-height:32px;height:32px;cursor:pointer;font-weight:bold;margin:0 0 1px 0;padding-left:5px;position:relative}#bank-library #bank-list .bank-item .remove{border-radius:0}#bank-library #bank-list .filter_{border-color:#111;border-style:dotted;border-width:0 1px 0 0;bottom:46px;left:0;padding-top:18px;position:fixed;top:0;right:0;width:240px;overflow:auto}#bank-library #bank-list .filter_ h1{font-weight:2em;text-transform:uppercase;font-weight:bold;margin:0 0 0 10px}#bank-library #bank-list .filter_ h2{color:white;font-size:15px;text-transform:uppercase;font-weight:bold;margin-top:1em;margin-left:10px}#bank-library #bank-list .filter_ .control-group{float:left;margin-right:9px;margin-left:10px}#bank-library #bank-list .filter_ .control-group label{color:#fff;display:inline-block;font-size:12px;font-weight:bold}#bank-library #bank-list .filter_ .control-group select{width:50px}#bank-library #bank-list .filter_>.form-group{padding:15px}#bank-library #bank-title{background:#111;left:20%;height:92px;margin:0;padding:0;position:fixed;text-align:left;top:45px;width:50%}#bank-library #bank-title h1{font-size:2em;padding:0;max-height:2em;display:block;margin:.5em .8em 0 .8em;padding-bottom:.2em;color:white;white-space:nowrap;overflow:hidden}#bank-library #bank-title form{margin:0 2em;color:#666}#bank-library #bank-title h2{margin:4px 0 0 14px;display:block;text-transform:initial !Important;color:white;font-size:12px;line-height:20px;text-align:left}#bank-library #bank-title small{font-size:10px;color:#bbb;text-align:left;display:block;margin-left:14px}#bank-library #bank-title select{display:block;float:left;height:22px;margin:5px 2.5%;width:20%;font-size:10px}#bank-library #bank-pedalboards{background:#111;bottom:46px;left:20%;list-style:none;margin:0;overflow-x:hidden;overflow-y:auto;padding:0 0 200px 0;position:fixed;top:164px;width:50%}#bank-library #bank-pedalboards>div{margin:0 10px 20px 20px}#bank-library #bank-pedalboards .pedalboard{color:#fff;cursor:-webkit-grab !important;cursor:grab !important;width:100%;transition:all .33s}#bank-library #bank-pedalboards .pedalboard:active{cursor:-webkit-grabbing !important;cursor:grabbing !important}#bank-library #bank-pedalboards .pedalboard:hover{background-color:#883996}#bank-library #bank-pedalboards .pedalboard .info{height:32px;padding-left:5px;background:rgba(255,255,255,0.07)}#bank-library #bank-pedalboards .pedalboard .title{display:block;line-height:32px;font-weight:bold;overflow:hidden;position:relative}#bank-library #bank-pedalboards .pedalboard .img{display:inline-block;float:left;height:auto;margin:0 9px 0 0;text-align:center;width:100%;background:url(../img/background.jpg);background-size:auto 100%;background-position:center center;box-shadow:inset 0 3px 6px black;border:1px solid #000}#bank-library #bank-pedalboards .mode{border:none;margin:22px 0 18px 0;text-align:center}#bank-library #bank-edit{position:fixed;display:block;background:#111 url(../img/watermark.png) 100% 100% no-repeat;width:80%;bottom:45px;top:45px;left:20%}#bank-library #bank-pedalboards-search{bottom:45px;right:0;overflow:auto;position:fixed;top:0;width:30%;z-index:6}#bank-library #bank-pedalboards-search h2{text-transform:uppercase;color:white;font-size:1.5em;font-weight:bold;float:left}#bank-library #bank-pedalboards-search .box{background:#111;padding:0;box-sizing:border-box}#bank-library #bank-pedalboards-search input{width:50%;padding-left:1em}#bank-library #bank-pedalboards-search .remove{display:none !important}#bank-library #bank-pedalboards-search label{color:#fff;font-weight:bold;text-transform:uppercase;float:left;line-height:45px;padding-left:10px}#bank-library #bank-pedalboards-search .control-group{margin-bottom:10px;padding:0 2%;position:relative;width:29.3%}#bank-library #bank-pedalboards-search input[type="search"]{width:50% !important;float:right;margin:5px}#bank-library #bank-pedalboards-search select{width:100%}#bank-library #bank-pedalboards-search .input-clean{background:#333;color:#fff;cursor:pointer;font-size:8px;height:18px;left:auto !important;position:absolute;right:8px !important;text-align:center;text-transform:uppercase;top:31px !important;width:18px;border-radius:100%}#bank-pedalboards-mode{background:#333;left:30%;height:30px;margin:0;padding:20px 0 0 0;position:absolute;text-align:center;top:68px;width:45%}#bank-pedalboards-mode .grid{background:url(../img/icn-grid-list.png) no-repeat left bottom;display:inline-block;height:14px;width:14px}#bank-pedalboards-mode .grid.selected{background-position:left top}#bank-pedalboards-mode .list{background:url(../img/icn-grid-list.png) no-repeat right bottom;display:inline-block;height:14px;width:14px}#bank-pedalboards-mode .list.selected{background-position:right top}.pedalboard .addressings{width:100%}.pedalboard .addressings .footswitch{background:#2b2b2b url(../img/footswitch-free.png) no-repeat center top;float:left;height:20px;width:25%}.pedalboard .addressings .footswitch.addressed{background:#2b2b2b url(../img/footswitch-addressed.png) no-repeat center top}#bank-pedalboards-result{position:fixed;bottom:45px;top:45px;overflow:auto;width:30%;background:#111}#bank-pedalboards-result .pedalboard{color:#fff;background:rgba(255,255,255,0.07);cursor:-webkit-grab;cursor:grab;margin:0 20px 20px 10px;transition:all .33s}#bank-pedalboards-result .pedalboard:active{cursor:-webkit-grabbing !important;cursor:grabbing !important}#bank-pedalboards-result .pedalboard:hover{background-color:#883996}#bank-pedalboards-result .pedalboard .img{display:block;height:auto;margin:0 auto;text-align:center;width:100%;background:url(../img/background.jpg);background-size:auto 100%;background-position:center center;box-shadow:inset 0 3px 6px black;border:1px solid #000}#bank-pedalboards-result .pedalboard .img img{max-width:100%}#bank-pedalboards-result .pedalboard .title{display:block;line-height:32px;font-weight:bold;overflow:hidden;position:relative;padding-left:5px}#bank-pedalboards-result .pedalboard .description{display:block;font-size:11px;height:27px;line-height:1.5;overflow:hidden}#file-manager-library{background:#2c2c2c url(../img/watermark.png) 100% 100% no-repeat;top:0;bottom:46px;left:0;right:0;overflow-x:hidden;overflow-y:auto;position:absolute;z-index:2}#file-manager-wrapper{display:flex;position:fixed;top:45px;bottom:55px;left:2em;right:2em;overflow:auto;padding-top:1%}#file-manager-wrapper iframe{flex:1;border:none}#pedalboards-library{background:#2c2c2c;bottom:45px;left:0;overflow-x:hidden;overflow-y:auto;position:absolute;right:0;top:0;z-index:2}#pedalboards-library h1{color:#fff;display:inline-block;font-size:24px;line-height:35px;margin:0 0 18px 0;padding:0}#pedalboards-library header .form-group{display:inline-block;float:right;margin-top:5px;margin-right:5px}#pedalboards-library header .form-group .form-control{width:200px !important}#pedalboards-library header .form-group .input-clean{line-height:20px;margin-right:12px !important}#pedalboards-library header .view-modes{float:right;margin-right:12px;cursor:pointer}#pedalboards-library header .view-modes .view-mode{font-size:130%}#pedalboards-library header .view-modes .view-mode.selected{color:#fff}#pedalboards-library .filter{border-color:#111;border-style:dotted;border-width:0 1px 0 0;bottom:45px;left:0;position:fixed;right:0;top:45px;width:240px}#pedalboards-library .filter .control-group{float:left;margin-right:9px}#pedalboards-library .filter .control-group label{color:#fff;display:inline-block;font-size:12px;font-weight:bold;width:165px}#pedalboards-library .filter .control-group select{width:50px}#pedalboards-library .pedalboards{left:0;position:fixed;right:0;top:45px;bottom:45px;overflow:auto;background:#111 url(../img/watermark.png) 100% 100% no-repeat;padding-top:1%}#pedalboards-library .pedalboards .pedalboard{color:#fff;float:left;margin:0 0 1% 1%;width:18.8%;position:relative;background-color:transparent;background-image:url(../img/background.jpg);background-position:center center;background-repeat:repeat;background-size:auto 100%;transition:all .33s;cursor:pointer}#pedalboards-library .pedalboards .pedalboard:hover .info{background-color:#883996}#pedalboards-library .pedalboards .pedalboard .img{box-shadow:inset 0 3px 6px black;border:1px solid #000;display:block;width:100%;height:10vw;overflow:hidden;text-align:center;background-image:url(../img/loading-pedalboard.gif);background-position:center center;background-repeat:no-repeat;background-size:auto;position:relative}#pedalboards-library .pedalboards .pedalboard .img.loaded{background-size:contain}#pedalboards-library .pedalboards .pedalboard .img.broken{background-image:url(../img/icons/broken_image.svg);background-size:auto 50%}#pedalboards-library .pedalboards .pedalboard.broken .img::after{content:"";position:absolute;top:0;left:0;right:0;bottom:0;z-index:10;background-color:rgba(17,17,17,0.5);background-image:url(../img/icons/broken_pedal.svg);background-size:auto 50%;background-repeat:no-repeat;background-position:center}#pedalboards-library .pedalboards .pedalboard .info{background-color:#2c2c2c;transition:background-color .33s}#pedalboards-library .pedalboards .pedalboard .title{display:block;line-height:32px;font-weight:bold;overflow:hidden;position:relative;height:32px;left:5px;width:calc(100% - 32px);text-overflow:ellipsis;white-space:nowrap}#pedalboards-library .pedalboards .pedalboard .description{display:block;font-size:12px;height:36px;line-height:1.5;overflow:hidden}#pedalboards-library .pedalboards .pedalboard .actions{font-weight:bold;text-transform:uppercase;position:absolute;top:0;right:0}#pedalboards-library .pedalboards .pedalboard .actions .js-remove{width:32px;height:32px;background-image:url(../img/icons/25/delete.png);background-repeat:no-repeat;background-position:50% 50%;transition:background-color .33s}#pedalboards-library .pedalboards .pedalboard .actions .js-remove:hover{background-color:#f29446}#pedalboards-library .pedalboards .pedalboard .actions>span{cursor:pointer}#pedalboards-library .pedalboards .pedalboard .actions>span:hover{text-decoration:underline}#pedalboards-library .pedalboards .pedalboard .actions>span.duplicate{margin:0 6px}#pedalboards-library .pedalboards.list-selected .img{display:none}#nprogress{pointer-events:none;z-index:103100}#nprogress .bar{background:#883996;position:fixed;z-index:103100;top:45px;left:0;opacity:.5;width:100%;height:2px}#nprogress .peg{display:none}#nprogress .spinner{display:none;position:fixed;z-index:1031;top:45px;right:0;background:rgba(0,0,0,0.5);padding:6px;box-sizing:border-box;transform:translate(-50% -50%)}#nprogress .spinner-icon{width:32px;height:32px;box-sizing:border-box;border:solid 3px transparent;border-top-color:#f29446;border-left-color:rgba(242,148,70,0.66);border-bottom-color:rgba(242,148,70,0.33);border-radius:50%;-webkit-animation:nprogress-spinner 400ms linear infinite;animation:nprogress-spinner 400ms linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .spinner,.nprogress-custom-parent #nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.mod-pedal-settings .mod-address.addressed,.mod-pedal-settings .preset-btn-assign-all.addressed{background-color:#a144b2}.mod-pedal-settings-address .mod-box .form-container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.mod-pedal-settings-address .mod-box .form-container .btn.js-type,.mod-pedal-settings-address .mod-box .form-container .btn.advanced-toggle{border:solid #333 1px}.mod-pedal-settings-address .mod-box .form-container .main-form-container{width:691px;padding-right:3px}.mod-pedal-settings-address .mod-box .form-container .main-form-container label{text-align:center}.mod-pedal-settings-address .mod-box .form-container .main-form-container .btn.js-type.selected{background-color:#333;color:white}.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike{display:flex;flex-basis:100%;align-items:center;color:black;margin:6px 0}.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike:before,.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike:after{content:"";flex-grow:1;background:black;height:3px;font-size:0;line-height:0}.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike:before{margin-right:8px}.mod-pedal-settings-address .mod-box .form-container .main-form-container .group-strike:after{margin-left:8px}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table{width:100%}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:first-child{background-color:#f66}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(2){background-color:#ff6}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(3){background-color:#6ff}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(4){background-color:#bb7cd5}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(5){background-color:#f5a77e}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(6){background-color:#6f6}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(7){background-color:#f6f}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th:nth-child(8){background-color:#8080ff}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table th,.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table td{border:solid #333 1px;text-align:center}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table td{padding:5px;cursor:pointer}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table td.selected{background-color:#883996;color:white;box-shadow:3px 3px 3px #999}.mod-pedal-settings-address .mod-box .form-container .main-form-container .hmi-table td.disabled{cursor:auto;color:#999;background-color:#d9d9d9;box-shadow:inset 0 0 3px #555}.mod-pedal-settings-address .mod-box .form-container .main-form-container .midi-custom-uri{color:#883996}.mod-pedal-settings-address .mod-box .form-container .buttons-container{padding-right:3px;padding-bottom:18px}.mod-pedal-settings-address .mod-box .form-container .advanced-container{border-left:1px solid #eee;padding-left:12px;margin-left:12px}.mod-pedal-settings-address .mod-box .form-container .advanced-container .cv-op-mode label{width:100% !important}.mod-pedal-settings-address .mod-box .form-container .advanced-container .cv-op-mode .mod-controls{width:155px !important}.mod-cv-output .output-cv-checkbox{display:flex;position:absolute;top:45px;left:15px}.mod-cv-output .output-cv-checkbox .checkbox-container{position:relative;padding-left:45px;cursor:pointer;font-size:22px;height:35px;width:35px}.mod-cv-output .output-cv-checkbox .checkbox-container .checkmark{position:absolute;top:0;left:0;height:35px;width:35px;background-color:#eee}.mod-cv-output .output-cv-checkbox .checkbox-container .checkmark:after{content:"";position:absolute;display:none;left:13px;top:7px;width:10px;height:17px;border:solid white;border-width:0 4px 4px 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]{position:absolute;opacity:0;cursor:pointer;z-index:5;width:100%;height:100%;top:0;left:0}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]:disabled{cursor:not-allowed}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]:checked~.checkmark{background-color:#883996}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]:checked~.checkmark:after{display:block}.mod-cv-output .output-cv-checkbox .checkbox-container input[type=checkbox]:disabled~.checkmark{background-color:#999}.mod-cv-output .output-cv-checkbox input[type=text]{font-size:20px}.mod-cv-output .output-cv-checkbox ::selection{background:#883996 !important;color:white !important}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-semibold-webfont.eot');src:url('../fonts/cooper/cooperhewitt-semibold-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-semibold-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-semibold-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-semibold-webfont.ttf') format('truetype');font-weight:600;font-style:normal;font-display:swap}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-light-webfont.eot');src:url('../fonts/cooper/cooperhewitt-light-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-light-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-light-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-light-webfont.ttf') format('truetype');font-weight:300;font-style:normal;font-display:swap}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-lightitalic-webfont.eot');src:url('../fonts/cooper/cooperhewitt-lightitalic-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-lightitalic-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-lightitalic-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-lightitalic-webfont.ttf') format('truetype');font-weight:300;font-style:italic;font-display:swap}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-bookitalic-webfont.eot');src:url('../fonts/cooper/cooperhewitt-bookitalic-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-bookitalic-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-bookitalic-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-bookitalic-webfont.ttf') format('truetype');font-weight:400;font-style:italic;font-display:swap}@font-face{font-family:'cooper hewitt';src:url('../fonts/cooper/cooperhewitt-book-webfont.eot');src:url('../fonts/cooper/cooperhewitt-book-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/cooper/cooperhewitt-book-webfont.woff2') format('woff2'),url('../fonts/cooper/cooperhewitt-book-webfont.woff') format('woff'),url('../fonts/cooper/cooperhewitt-book-webfont.ttf') format('truetype');font-weight:400;font-style:normal;font-display:swap}*{outline:0 !important}html,body{height:100%;overflow:hidden}body{background:#111;min-width:1024px}body.ios{position:fixed;min-width:980px}*{font-family:"cooper hewitt",Sans-serif !important}.unpadded{padding:0 !important}.unpadded-r{padding:0 0 0 7px}.unpadded-l{padding:0 7px 0 0}.ui-draggable:active{cursor:-webkit-grabbing !important;cursor:grabbing !important}.form-control{width:100% !important}#wrapper{bottom:0;left:0;min-width:980px;overflow:hidden;position:absolute;right:0;top:0}hr.dotted{border:1px dotted #444;margin:2px 0 3px 0}.preset-list{width:100%;height:200px !important}.input-clean{background:#333;color:#fff;cursor:pointer;height:18px;left:auto !important;position:absolute;right:0;margin-right:18px !important;text-align:center;text-transform:uppercase;margin-top:-26px !important;width:18px;border-radius:100%}.input-clean:after{content:"×";content:"\00d7";font-size:14px;font-weight:bold}.form-control::selection{background:#883996 !important;color:white !important}#notifications{position:absolute;top:55px;right:0;width:320px;z-index:100000}#notifications>section{border-radius:0;margin-bottom:0 !important;opacity:.96;cursor:pointer}#notifications .progressbar{border:0;height:8px;overflow:hidden;position:absolute;left:8px;bottom:0;width:306px}#notifications .progressbar .progressbar-value{background-color:rgba(136,57,150,0.7);height:90%;margin:0}#main-menu{background:#111;bottom:0;color:#fff;height:45px;left:0;position:absolute;right:0;z-index:10}#main-menu>div{cursor:pointer;float:left}#main-menu>div.icon{height:45px;width:45px}#main-menu>div.icon>img{margin:5px 0 0 6px}#main-menu>div.icon.selected{background:#000}#main-menu>div.icon>a{display:block;height:45px;width:45px}#main-menu #mod-plugins{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-45px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-plugins:hover{background-color:#000;background-position:-45px bottom}#main-menu #mod-plugins.selected{background-color:#000;background-position:-45px bottom}#main-menu #mod-cloud-plugins{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-315px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-cloud-plugins:hover{background-color:#000;background-position:-315px bottom}#main-menu #mod-cloud-plugins.selected{background-color:#000;background-position:-315px bottom}#main-menu #mod-pedalboard{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-90px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-pedalboard:hover{background-color:#000;background-position:-90px bottom}#main-menu #mod-pedalboard.selected{background-color:#000;background-position:-90px bottom}#main-menu #mod-bank{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-135px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-bank:hover{background-color:#000;background-position:-135px bottom}#main-menu #mod-bank.selected{background-color:#000;background-position:-135px bottom}#main-menu #mod-file-manager{background-image:url(../img/menu-icons-sprite.png?v=2);background-position:-180px top;background-repeat:no-repeat;transition:all .33s}#main-menu #mod-file-manager:hover{background-color:#000;background-position:-180px bottom}#main-menu #mod-file-manager.selected{background-color:#000;background-position:-180px bottom}#main-menu #banks-saving{margin:21px 0 0 6px;font-size:10px;color:#666;display:none}#main-menu #mod-show-midi-port,#main-menu #mod-transport-icon,#main-menu .mod-bypass,#main-menu #mod-cpu,#main-menu #mod-ram,#main-menu #mod-xruns,#main-menu #mod-buffersize,#main-menu #mod-cpu-stats{background-position:8px 50%;background-repeat:no-repeat;padding-left:40px;padding-right:12px;color:#fff;float:right;font-size:11px;font-weight:bold;line-height:4.5;text-transform:uppercase;width:auto !important;transition:all .33s}#main-menu #mod-show-midi-port:hover,#main-menu #mod-transport-icon:hover,#main-menu .mod-bypass:hover,#main-menu #mod-cpu:hover,#main-menu #mod-ram:hover,#main-menu #mod-xruns:hover,#main-menu #mod-buffersize:hover,#main-menu #mod-cpu-stats:hover{background-color:#000}#main-menu #mod-show-midi-port.selected,#main-menu #mod-transport-icon.selected,#main-menu .mod-bypass.selected,#main-menu #mod-cpu.selected,#main-menu #mod-ram.selected,#main-menu #mod-xruns.selected,#main-menu #mod-buffersize.selected,#main-menu #mod-cpu-stats.selected{background-color:#333}#main-menu #mod-show-midi-port{background-image:url(../img/icons/25/midi.png)}#main-menu #mod-transport-icon{background-image:url(../img/icons/25/stop.png);font-family:monospace;width:10.5em !important;text-align:right}#main-menu #mod-transport-icon.playing{background-image:url(../img/icons/25/transport.png)}#main-menu .mod-bypass{background-image:url(../img/icons/25/bypass.png)}#main-menu .mod-bypass.bypassed{background-color:#883996}#main-menu #mod-cpu,#main-menu #mod-ram{cursor:default;width:115px !important;padding:0 !important;background:linear-gradient(to right, #1e5799 0, #2989d8 69%, #ff6430 78%, #ff6430 93%, #c40000 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#1e5799', endColorstr='#c40000', GradientType=1)}#main-menu #mod-cpu .progress-title{background-image:url(../img/icons/25/cpu.png);background-position:0 50%;background-repeat:no-repeat;padding-left:30px}#main-menu #mod-ram .progress-title{background-image:url(../img/icons/25/ram.png);background-position:0 50%;background-repeat:no-repeat;padding-left:30px}#main-menu #mod-xruns{background-image:url(../img/icons/25/cpu.png);width:115px !important}#main-menu #mod-cpu-stats{background-image:url(../img/icons/25/cpu.png);width:155px !important;text-transform:none}#main-menu #mod-buffersize{background-image:url(../img/icons/25/cpu.png);width:125px !important}#main-menu .progress{height:45px;margin-bottom:0;overflow:hidden;float:right;background-color:#111;-webkit-transition:width 1s;transition:width 1s}#main-menu .progress .bar{float:right;background:#222 !important}#main-menu .progress-title{position:absolute;top:0;height:45px;margin:0;margin-left:1em;color:white}#main-menu .status{background-color:#111;background-position:3px 50%;background-size:36px;background-repeat:no-repeat;border-color:#2b2b2b;border-style:solid;border-width:0 0 0 1px;float:right}#main-menu #mod-settings{font-family:fontello !important;font-size:28px;padding-top:3px;padding-left:10px;position:relative;color:#fff;opacity:.66;cursor:pointer;transition:all .33s}#main-menu #mod-settings:after{content:'\e802'}#main-menu #mod-settings:hover{opacity:1}#main-menu #mod-update{background-image:url(../img/icons/36/update.png)}#main-menu #mod-update.uptodate{background-image:url(../img/icons/36/ok.png)}#main-menu #mod-status{background-image:url(../img/icons/36/disconnect.png)}#main-menu #mod-status.online{background-image:url(../img/icons/36/disconnect.png)}#main-menu #mod-devices{background-image:url(../img/icons/36/control-chain.png)}#cloud_install_all{background-image:url(../img/icons/36/save.png);float:right;font-size:21px}#cloud_update_all{background-image:url(../img/icons/36/update.png);float:right;font-size:21px}header#pedalboard-actions,#bank-library header,#cloud-plugins-library header,#pedalboards-library header,#file-manager-library header{display:block;left:0;height:45px;line-height:45px;position:fixed;right:0;top:0;box-shadow:0 1px 10px rgba(0,0,0,0.1);z-index:5;background-color:#111;background-position:3px 50%;background-size:36px;background-repeat:no-repeat;background-image:url(../img/icons/36/mod.png);padding-left:45px}header#pedalboard-actions .help,#bank-library header .help,#cloud-plugins-library header .help,#pedalboards-library header .help,#file-manager-library header .help{background-image:url(../img/icn-help-16x16.png);background-position:center center;background-repeat:no-repeat;display:inline-block;height:21px;width:21px}header#pedalboard-actions .close,#bank-library header .close,#cloud-plugins-library header .close,#pedalboards-library header .close,#file-manager-library header .close{opacity:1;position:absolute;right:10px;top:16px}header#pedalboard-actions .drop-menu,#bank-library header .drop-menu,#cloud-plugins-library header .drop-menu,#pedalboards-library header .drop-menu,#file-manager-library header .drop-menu{position:absolute;display:none;list-style-type:none;padding:0;background-color:rgba(0,0,0,0.8);color:#999;width:150px}header#pedalboard-actions .drop-menu:hover,#bank-library header .drop-menu:hover,#cloud-plugins-library header .drop-menu:hover,#pedalboards-library header .drop-menu:hover,#file-manager-library header .drop-menu:hover{display:block}header#pedalboard-actions .drop-menu li,#bank-library header .drop-menu li,#cloud-plugins-library header .drop-menu li,#pedalboards-library header .drop-menu li,#file-manager-library header .drop-menu li{padding:5px 20px}header#pedalboard-actions .drop-menu li:hover,#bank-library header .drop-menu li:hover,#cloud-plugins-library header .drop-menu li:hover,#pedalboards-library header .drop-menu li:hover,#file-manager-library header .drop-menu li:hover{color:white;background-color:#883996}header#pedalboard-actions .drop-menu .sub-menu,#bank-library header .drop-menu .sub-menu,#cloud-plugins-library header .drop-menu .sub-menu,#pedalboards-library header .drop-menu .sub-menu,#file-manager-library header .drop-menu .sub-menu{display:none;list-style-type:none;padding:0;position:absolute;left:150px;height:14em;overflow-y:auto;overflow-x:hidden;margin-top:-50px;background-color:rgba(0,0,0,0.8);width:15em;border-left:3px solid #666}header#pedalboard-actions .drop-menu .sub-menu:hover,#bank-library header .drop-menu .sub-menu:hover,#cloud-plugins-library header .drop-menu .sub-menu:hover,#pedalboards-library header .drop-menu .sub-menu:hover,#file-manager-library header .drop-menu .sub-menu:hover{display:block}header#pedalboard-actions .menu-trigger:hover+.drop-menu,#bank-library header .menu-trigger:hover+.drop-menu,#cloud-plugins-library header .menu-trigger:hover+.drop-menu,#pedalboards-library header .menu-trigger:hover+.drop-menu,#file-manager-library header .menu-trigger:hover+.drop-menu{display:block}header#pedalboard-actions .sub-menu-trigger:hover ul,#bank-library header .sub-menu-trigger:hover ul,#cloud-plugins-library header .sub-menu-trigger:hover ul,#pedalboards-library header .sub-menu-trigger:hover ul,#file-manager-library header .sub-menu-trigger:hover ul{display:block}header#pedalboard-actions button,#bank-library header button,#cloud-plugins-library header button,#pedalboards-library header button,#file-manager-library header button{background-position:8px 50%;background-size:25px;background-repeat:no-repeat;padding:0 12px 0 40px;background-color:transparent;border:none;height:45px;line-height:45px;opacity:.66;color:white;transition:all .33s}header#pedalboard-actions button:hover,#bank-library header button:hover,#cloud-plugins-library header button:hover,#pedalboards-library header button:hover,#file-manager-library header button:hover,header#pedalboard-actions button.selected,#bank-library header button.selected,#cloud-plugins-library header button.selected,#pedalboards-library header button.selected,#file-manager-library header button.selected{opacity:1;background-color:black}header#pedalboard-actions button.js-save,#bank-library header button.js-save,#cloud-plugins-library header button.js-save,#pedalboards-library header button.js-save,#file-manager-library header button.js-save{background-image:url(../img/icons/25/save.png)}header#pedalboard-actions button.js-save-as,#bank-library header button.js-save-as,#cloud-plugins-library header button.js-save-as,#pedalboards-library header button.js-save-as,#file-manager-library header button.js-save-as{background-image:url(../img/icons/25/save.png)}header#pedalboard-actions button.js-preset,#bank-library header button.js-preset,#cloud-plugins-library header button.js-preset,#pedalboards-library header button.js-preset,#file-manager-library header button.js-preset{background-image:url(../img/icons/36/presets.png);background-size:30px 30px}header#pedalboard-actions button#js-preset-enabler:hover,#bank-library header button#js-preset-enabler:hover,#cloud-plugins-library header button#js-preset-enabler:hover,#pedalboards-library header button#js-preset-enabler:hover,#file-manager-library header button#js-preset-enabler:hover,header#pedalboard-actions button#js-preset-enabler.selected,#bank-library header button#js-preset-enabler.selected,#cloud-plugins-library header button#js-preset-enabler.selected,#pedalboards-library header button#js-preset-enabler.selected,#file-manager-library header button#js-preset-enabler.selected{background-color:#883996}header#pedalboard-actions button.js-reset,#bank-library header button.js-reset,#cloud-plugins-library header button.js-reset,#pedalboards-library header button.js-reset,#file-manager-library header button.js-reset{background-image:url(../img/icons/25/new.png)}header#pedalboard-actions button.js-reset:hover,#bank-library header button.js-reset:hover,#cloud-plugins-library header button.js-reset:hover,#pedalboards-library header button.js-reset:hover,#file-manager-library header button.js-reset:hover,header#pedalboard-actions button.js-reset.selected,#bank-library header button.js-reset.selected,#cloud-plugins-library header button.js-reset.selected,#pedalboards-library header button.js-reset.selected,#file-manager-library header button.js-reset.selected{background-color:#883996}header#pedalboard-actions button.js-cv-addressing,#bank-library header button.js-cv-addressing,#cloud-plugins-library header button.js-cv-addressing,#pedalboards-library header button.js-cv-addressing,#file-manager-library header button.js-cv-addressing{background-image:url(../img/icons/25/cv.png)}header#pedalboard-actions button.js-cv-addressing:hover,#bank-library header button.js-cv-addressing:hover,#cloud-plugins-library header button.js-cv-addressing:hover,#pedalboards-library header button.js-cv-addressing:hover,#file-manager-library header button.js-cv-addressing:hover,header#pedalboard-actions button.js-cv-addressing.selected,#bank-library header button.js-cv-addressing.selected,#cloud-plugins-library header button.js-cv-addressing.selected,#pedalboards-library header button.js-cv-addressing.selected,#file-manager-library header button.js-cv-addressing.selected{background-color:#883996}header#pedalboard-actions button.js-cloud,#bank-library header button.js-cloud,#cloud-plugins-library header button.js-cloud,#pedalboards-library header button.js-cloud,#file-manager-library header button.js-cloud{background-image:url(../img/icn-share.png);background-size:32px;background-position:5px center}header#pedalboard-actions h1,#bank-library header h1,#cloud-plugins-library header h1,#pedalboards-library header h1,#file-manager-library header h1{font-weight:2em;text-transform:uppercase;font-weight:normal;color:#999;display:inline-block;font-size:24px;line-height:49px;height:45px;padding:0}header#pedalboard-actions{box-shadow:0 2px 12px black}.tooltip{background:white;bottom:50px;height:auto;padding:7px;position:absolute}.tooltip .text{color:#222;font-weight:bold;line-height:1;text-align:center}.tooltip .arrow{border-bottom:9px solid transparent;border-left:9px solid transparent;border-right:9px solid transparent;border-top:9px solid white;height:0;position:absolute;bottom:-18px;right:14px;width:0}#mod-devices-window,#mod-upgrade{background:white;color:black;font-size:15px;max-height:350px;overflow:auto;position:absolute;right:10px;bottom:50px;z-index:100}#mod-devices-window .progressbar-wrapper,#mod-upgrade .progressbar-wrapper{border:1px solid #444;height:20px;width:330px}#mod-devices-window .progressbar,#mod-upgrade .progressbar{background-color:#88f;height:18px}#mod-devices-window{max-width:600px;min-width:500px}#mod-upgrade{max-width:450px;min-width:350px}#mod-transport-window{bottom:48px;right:310px;top:initial;left:initial;background:rgba(17,17,17,0.9);padding:50px 40px 15px 40px;z-index:999}#mod-transport-window .mod-midi-icon{width:auto !important;padding-left:25px;background-position:5% 45% !important;background-image:url(../img/icons/25/midi.png);background-size:15px;background-repeat:no-repeat}#plugins-library{bottom:45px;height:166px;left:0;position:absolute;right:0;transition:all .33s;transition:opacity .5s;transition:height .5s}#plugins-library.folded{height:38px}#plugins-library.folded #plugins-library-settings-window{bottom:0}#plugins-library.fade{opacity:.2}#plugins-library .fold{cursor:pointer;display:block !important;float:right;height:36px;position:absolute;top:0;right:0;width:36px;background-color:rgba(17,17,17,0.66);background-image:url(../img/icons/25/pinned.png);background-position:center center;background-repeat:no-repeat}#plugins-library .folded{cursor:pointer;display:block;float:right;height:36px;position:absolute;top:0;right:0;width:36px}#plugins-library.auto .fold{background-image:url(../img/icons/25/autohide.png)}#plugins-library .settings{background-color:rgba(17,17,17,0.66);background-image:url(../img/icn-search-white.png);background-position:center center;background-repeat:no-repeat;cursor:pointer;display:block;height:36px;position:absolute;right:37px;top:0;width:36px}#plugins-library #plugins-library-settings-window{background-color:rgba(17,17,17,0.66);bottom:130px;color:#fff;position:absolute;right:74px;transition:bottom .5s}#plugins-library #plugins-library-settings-window h1{padding:0}#plugins-library #plugins-library-settings-window form{padding:4px;border:0;margin:0}#plugins-library #plugins-library-settings-window label{margin-right:10px}#plugins-library #plugins-library-settings-window label.checkbox:last-child{margin-right:0}#plugins-library #plugins-library-settings-window .control-group{position:relative}#plugins-library #plugins-library-settings-window input{width:120px;height:28px;line-height:28px;padding:0 6px;outline:0;border:0}#plugins-library #plugins-library-settings-window .input-clean{position:absolute;top:50%;right:8px;bottom:auto;left:auto;transform:translateY(-50%);margin:0 !important;border-radius:100%}#plugins-library ul{list-style:none;margin:6px 73px 0 0;overflow:hidden;padding:10px 0 0 0;position:relative;width:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}#plugins-library ul li{background:rgba(17,17,17,0.66);color:#bbb;cursor:pointer !important;display:inline-block;float:left;font-size:11px;margin:0 1px 0 0;padding:3px 11px;text-shadow:1px 1px 0 #000;text-transform:uppercase;border-top:solid 3px transparent;transition-property:border-radius,color,margin-top,padding,border-top;transition-duration:.33s}#plugins-library ul li:hover{margin-top:-6px;padding:6px 11px;color:white}#plugins-library ul li.selected{color:white;font-weight:bold;margin-top:-6px;padding:6px 11px;border-top:solid 3px #883996}#plugins-library.scrolling .plugins-wrapper{transition:all .1s !important}#plugins-library #plugins-list{background:rgba(17,17,17,0.66);height:140px;overflow:hidden;position:relative;width:100%;z-index:1}#plugins-library #plugins-list .nav{background:url(../img/nav-left.png) 50% 50% no-repeat;cursor:pointer;position:absolute;top:0;width:50px;height:100%;opacity:.66}#plugins-library #plugins-list .nav:hover{opacity:1}#plugins-library #plugins-list .nav-left{left:0}#plugins-library #plugins-list .nav-right{background:url(../img/nav-right.png) 50% 50% no-repeat;right:0}#plugins-library #plugins-list #plugins-results{bottom:0;left:50px;position:absolute;overflow:hidden;right:50px;top:0}#plugins-library #plugins-list .plugins-wrapper{position:absolute;display:inline-block;white-space:nowrap;transition:all .5s;top:120px;left:0;opacity:0}#plugins-library #plugins-list .plugins-wrapper.selected{top:0;opacity:1}#plugins-library #plugins-list .plugins-wrapper .plugin{color:#fff;font-size:11px;font-weight:bold;height:108px;padding:14px 12px 10px;position:relative;text-align:center;display:inline-block}#plugins-library #plugins-list .plugins-wrapper .plugin .thumb{height:67px;cursor:pointer !important;position:relative;display:flex;flex-direction:column;justify-content:center}#plugins-library #plugins-list .plugins-wrapper .plugin .thumb:active{cursor:-webkit-grabbing !important;cursor:grabbing !important}#plugins-library #plugins-list .plugins-wrapper .plugin .thumb img{height:auto;margin:0 auto 3px;width:auto;pointer-events:none;max-width:256px;max-height:64px}#plugins-library #plugins-list .plugins-wrapper .plugin .status{display:block;height:16px;margin:0 auto;width:16px}#plugins-library #plugins-list .plugins-wrapper .plugin .status.demo{display:inline-block;width:auto;height:auto;margin:0}#plugins-library #plugins-list .plugins-wrapper .plugin .status.installed{background-image:url(../img/icn-installed.png);background-position:center center;background-repeat:no-repeat}#plugins-library #plugins-list .plugins-wrapper .plugin .status.outdated{background-image:url(../img/icn-outdated.png);background-position:center center;background-repeat:no-repeat}#plugins-library #plugins-list .plugins-wrapper .plugin .status.blocked{background-image:url(../img/icn-blocked.png);background-position:center center;background-repeat:no-repeat}#plugins-library #plugins-list .plugins-wrapper .plugin .author{display:block;line-height:1.5}#plugins-library #plugins-list .plugins-wrapper .plugin .title{display:block;line-height:1.25}#plugins-library #plugins-list .plugins-wrapper .plugin .rating{background-image:url(../img/icn-rating.png);background-position:center top;background-repeat:no-repeat;display:block;height:16px;width:132px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.one{background-position:center -15px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.two{background-position:center -30px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.three{background-position:center -45px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.four{background-position:center -60px}#plugins-library #plugins-list .plugins-wrapper .plugin .rating.five{background-position:center -75px}#plugins-library #plugins-list .plugins-wrapper .plugin .downloads{display:block}#plugins-library #plugins-list .plugins-wrapper .plugin .demo-container{position:absolute;left:0;bottom:3px;right:0;text-align:center}.plugin-info{background-color:rgba(20,20,20,0.9);bottom:0;color:#ccc;left:0;overflow-y:scroll;position:fixed;right:0;top:0;z-index:40}.plugin-info .box{margin-top:3em;margin-bottom:3em;background-color:#111;padding:15px;border:2px solid #333}.plugin-info .box .row-fluid{position:relative}.plugin-info .close{color:#fff;font-size:12px;font-weight:bold;opacity:1;position:absolute;right:10px;text-shadow:none;text-transform:uppercase;top:10px}.plugin-info .screenshot{padding:18px;min-width:3em;min-height:9em;display:inline-block;z-index:0}.plugin-info .screenshot div.beta{background-color:#ff2927;font-family:"cooper hewitt",sans-serif;font-weight:700;padding:5px 10px;color:white;position:absolute;top:18px;left:30px;font-size:16px}.plugin-info figure{position:relative;min-height:1px;padding-right:15px;padding-left:15px}@media (min-width:992px){.plugin-info figure{float:left;width:33.33333333%}}.plugin-info figure:hover{z-index:10 !important}.plugin-info .plugin-description{position:relative;min-height:1px;padding-right:15px;padding-left:15px;height:100%;display:block;position:a;padding-left:45px;background:-moz-linear-gradient(left, rgba(17,17,17,0.1) 1em, #111 2em);background:-webkit-gradient(linear, left top, right top, color-stop(1em, rgba(17,17,17,0.1)), color-stop(2em, #111));background:-webkit-linear-gradient(left, rgba(17,17,17,0.1) 1em, #111 2em);background:-o-linear-gradient(left, rgba(17,17,17,0.1) 1em, #111 2em);background:-ms-linear-gradient(left, rgba(17,17,17,0.1) 1em, #111 2em);background:linear-gradient(to right, rgba(17,17,17,0.1) 1em, #111 2em);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffffff', endColorstr='#ffffff', GradientType=1)}@media (min-width:992px){.plugin-info .plugin-description{float:left;width:58.33333333%}}.plugin-info .plugin-description>p{font-size:16px;margin-bottom:27px;white-space:pre-wrap}.plugin-info header{padding:18px 0 0}.plugin-info header h1{display:inline-block;vertical-align:middle;color:#fff;font-size:27px;font-weight:bold;line-height:1;margin:0;padding:0}.plugin-info header h1 span{font-size:16px;line-height:1;margin:0;padding:0}.plugin-info .version{color:#ccc;font-size:12px;font-weight:bold;text-transform:uppercase}.plugin-info .version span.bold{color:#fff;font-size:14px}.plugin-info .version button{font-weight:bold;margin-top:6px;text-transform:uppercase}.plugin-info .rating{background:url(../img/icn-rating.png) no-repeat -12px top;display:inline-block;height:14px;width:84px}.plugin-info .rating.one{background:url(../img/icn-rating.png) no-repeat -12px -15px}.plugin-info .rating.two{background:url(../img/icn-rating.png) no-repeat -12px -30px}.plugin-info .rating.three{background:url(../img/icn-rating.png) no-repeat -12px -45px}.plugin-info .rating.four{background:url(../img/icn-rating.png) no-repeat -12px -60px}.plugin-info .rating.five{background:url(../img/icn-rating.png) no-repeat -12px -75px}.plugin-info .rating-label{color:#fff;font-size:12px;font-weight:bold;text-transform:uppercase}.plugin-info .my-rate{width:15px;height:15px;margin:0;padding:0;float:left;cursor:pointer}.plugin-info .plugin-specs{text-align:center;width:100%;border-color:#333}.plugin-info .plugin-specs th{background:#333;color:#fff;border-color:#333;text-align:center}.plugin-info .plugin-specs td{border-color:#333}.plugin-info .plugin-specs td:first-child{width:40%}.plugin-info .plugin-specs td:not(first-child){width:20%}.plugin-info .comments-reviews{border-top:1px solid #333;margin-top:10px;padding-top:10px}.plugin-info .comments-reviews h1{color:#fff;font-size:21px;line-height:2;margin:0;padding:0}.plugin-info .reviews{border-bottom:1px solid #333;margin:9px 0 18px 0;padding:9px 0 18px 0}.plugin-info article.review{margin:18px 0}.plugin-info article.review h1{color:#fff;font-size:14px}.plugin-info article.review h1 .rating{margin-left:6px}.plugin-info article.review h1 .rating-label{color:#ccc;display:inline;font-size:12px;font-weight:normal;margin-left:6px}.plugin-info article.review .bar{background:#333;display:block;height:12px;margin:2px 6px 0;position:relative;width:100px}.plugin-info article.review .progress-bar{background:#444;bottom:0;display:block;height:10px;left:0;margin:1px;position:absolute;right:0;top:0;width:50%}.plugin-info article.review .rating-number{display:block;font-size:12px;margin-top:-1px}.plugin-info article.comment{border-top:1px solid #333;margin:18px 0 0;padding:18px 0 0}.plugin-info article.comment h1{color:#fff;font-size:16px;line-height:1}.plugin-info article.comment h1 .date{color:#ccc;font-size:12px;font-weight:normal;margin-left:6px}.plugin-info .comments{margin:18px 0;padding:18px 0}.plugin-info .comments button{margin-top:18px}.plugin-info form.comment-form{border-top:1px solid #333;margin:18px 0;padding:18px 0}.plugin-info form.comment-form label{font-size:12px;font-weight:bold;text-transform:uppercase}.plugin-info form.comment-form textarea{height:180px}.plugin-info .favorite-button{display:inline-block;vertical-align:middle;width:30px;height:30px;background:url(../img/icons/25/star-border.png) no-repeat center center;cursor:pointer;opacity:.5}.plugin-info .favorite-button:hover{opacity:.8}.plugin-info .favorite-button.favorite{opacity:1;background:url(../img/icons/25/star.png) no-repeat center center}.plugin-info .favorite-button.favorite:hover{opacity:1}.plugin-info .online-button-href{display:inline-block;margin-bottom:10px}.plugin-info .status.demo{display:inline-block;padding:4px 8px;margin:2px 0;font-size:.8em}.clearfix:after,.container:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after{content:"\0020";display:block;height:0;clear:both;visibility:hidden;overflow:hidden}.clearfix,.container,.dl-horizontal dd,.container,.container-fluid,.row,.form-horizontal .form-group,.btn-toolbar,.btn-group-vertical>.btn-group,.nav,.navbar,.navbar-header,.navbar-collapse,.pager,.panel-body{display:block}.clear{clear:both}.blend{opacity:.25}.box{padding:10px}.alignleft{float:left}.alignright{float:right}.textcenter{text-align:center}.textright{text-align:right}.bottom{margin-bottom:0 !important;padding-bottom:0 !important}.left{margin-left:0 !important;padding-left:0 !important}.right{margin-right:0 !important;padding-right:0 !important}.top{margin-top:0 !important;padding-top:0 !important}.nobullets{list-style:none}.mod-hidden{display:none}.mod-init-hidden{opacity:0}::selection{background:transparent}.mod-box{background:white;left:50%;margin:-150px -168px;padding:18px;position:absolute;top:50%;width:300px}.mod-box:not(#midi-ports-box) label{display:inline-block;float:left;line-height:2;width:96px}.mod-box .mod-controls{float:left}.mod-box input[type="text"]{width:190px}.mod-box input[type="password"]{width:190px}.blocker{background:rgba(17,17,17,0.9);color:white;display:block;position:fixed;width:100% !important;height:100% !important;top:0;left:0;text-align:center;z-index:99999}.blocker p{position:absolute;top:50%;width:100%;margin-top:56px;font-size:24px}.blocker.screen-disconnected{background:rgba(17,17,17,0.9) url(../img/icons/blocked.svg) 50% 50% no-repeat;background-size:100px 100px}.blocker.screen-disconnected .button{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);margin-top:120px}.plugin-wait{background:url(../img/loading-effect.gif) no-repeat center center;border:1px solid #ccc}.button{background:#883996;color:white;display:inline-block;cursor:pointer;line-height:25px;padding:5px 15px;transition:all .33s;border:none}.button:hover{background-color:#f29446;box-shadow:0 1px 5px black}.button.icon{padding-left:40px;background-position:10px 50%;background-repeat:no-repeat;background-size:25px}.drag-handle{bottom:0;cursor:-webkit-grabbing !important;cursor:grabbing !important;left:0;position:absolute;right:0;top:0;z-index:20}.demo-plugin{background:url("/img/trial.svg") no-repeat center center;background-size:contain;opacity:.4;z-index:11 !important}.demo-mask{width:100%;height:100%;left:0;right:0;top:0;bottom:0;position:absolute}.demo-plugin-light{opacity:.1 !important}.actions{z-index:20}@keyframes flashlight{0%{box-shadow:0 0 0 transparent}50%{box-shadow:0 0 10px white}100%{box-shadow:0 0 0 transparent}}.alert{background:rgba(17,17,17,0.9);border-width:0 0 0 6px;border-color:#883996;color:white;box-shadow:0 3px 10px black;margin-top:5px !important}.alert .js-close{color:white}.alert-warning{border-color:#f29446}.alert-danger{border-color:#c00}#go-back{width:200px;text-align:left;margin:20px 0 0 0;background:none;border:none;font-size:15px;padding:5px;color:#fff !important}#go-back span{font-family:fontello !important}#go-back span:before{content:'\e804'}#go-back:hover{text-decoration:none}.unstable-warning{float:left;background-color:#ffebb1;padding:5px;border-radius:10px} +/* TONE3000 */ +#main-menu #mod-tone3000{background-image:url(../img/tone3000-icon.png);background-position:center center;background-size:30px 30px;background-repeat:no-repeat;transition:all .33s} +#main-menu #mod-tone3000:hover{background-color:#000} +#main-menu #mod-tone3000.selected{background-color:#000} +#tone3000-library{background:#2c2c2c url(../img/watermark.png) 100% 100% no-repeat;top:0;bottom:46px;left:0;right:0;overflow-x:hidden;overflow-y:auto;position:absolute;z-index:2} +#tone3000-library header{display:block;left:0;height:45px;line-height:45px;position:fixed;right:0;top:0;box-shadow:0 1px 10px rgba(0,0,0,0.1);z-index:5;background-color:#111;background-position:3px 50%;background-size:36px;background-repeat:no-repeat;background-image:url(../img/icons/36/mod.png);padding-left:45px} +#tone3000-library header h1{font-weight:2em;text-transform:uppercase;font-weight:normal;color:#999;display:inline-block;font-size:24px;line-height:49px;height:45px;padding:0} +#tone3000-wrapper{position:absolute;top:45px;bottom:46px;left:0;right:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:#ccc} +#tone3000-logo{width:96px;height:96px} +#tone3000-wrapper h2{color:#fff;font-size:24px;font-weight:normal;text-transform:uppercase;margin:.8em 0 .2em} +#tone3000-wrapper .tone3000-hint{font-size:13px;line-height:1.5;color:#999;max-width:34em;margin:0 0 1em} +#tone3000-wrapper .tone3000-hint b{color:#ccc;font-weight:normal} +#tone3000-wrapper .tone3000-hint:last-of-type{margin-bottom:1.5em} diff --git a/html/img/tone3000-icon.png b/html/img/tone3000-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e5615d8c8f53935f7bcbbdc4d384d5b91250c020 GIT binary patch literal 3445 zcmcgv`8O1bA5~su8T5o0g-8lxXR=ol5<(b8_OTmF)=0_z7;6~W8)a?mV;kEO&o(h5 znrWCJGPY^#BM(O|f`n#KQ&TVq;_DF@A9W@o&cdHyr1G zZ{6|pD!+9dd@tUAweRg`4MooBtnC+>aFem};52;l%?__e>|N+n+iHLN7oYPefdl zm-;^~T)kVbT)}1mlO16r$YH+)q8BwC{zCYSO7E;nbgOr0enP z;2b(UOFYi*mszwBH*6cTA#Vjt;2@p5Aojk>IPLOqEuIx!eCjZ)H8G}!#)uwvf}2a- z5>X-ZjK+Oo>i}8&ys`K}^ZDJ#@Ku)5cbI$2Nk1-`?hoa5ybAaw$UupXO}LFu4O|jO z0`2SNUQ0WFjLz8@$i(o${;cQFti_@0-Xbx!cG*HI*nF{YW8om$pdz;W&m)no^RZ}9 zYR#h*%o)tZh$^NUg!~OzJGP^+;(E{=2TO;I+xgo~ zV(dsg7%|M;A))E8uk}l29%0(!-`fGN06i_fm1sIBsISysUZsDq6O)7CY_Bk&J-UBW zH0ql3x92w2o6U!(i>?Wchs}#DxSs1(cnRDQPH(OTjP^N_GtJzXpYhKo!zVs!g?l=m z6%f=R=qpAz)hM>015MsNc!-e5Z!!ayKnz)4zvg;_TkOPJpp7W)x8OO8>E0e==Qxgl zaz%%mD`%o`sdkEu$954dq$Vy?g^&+(PXxeZy~d9$Qg}CmK>2`cD~ zbH_KRy^fY$;i+fOItW4ri1kx3tgkeJ9VDcID0DOfcjJt;U;C9TAF^KX&kAiIj6pWf z&dmIvov4KM3oBrC6ahdb3lz*0Ci*)AI(Us%RVE9(ZDC2nZHxu~O{~8hk>ULc31C)K z2_>%%7g&^09So}eHBpJ~;C6Et#s$mD?^QyQwWbbP13t`|`N?|6p0$})m+xZ_;3X;) zqB;z2*Tu4%OE?NWXe{TvB#@X0ljh@r)_gvk_{IRQ|ov&~UsQmFPC^!XX>$D&%@lCWAraJ2r#yr$?>;bxs@H}U>u7&5% zR!^RGsP_zK1zSCyvD{tu8UsM2Bo%_Hs@l|fm~|_C9^a&f)7nu~af6VR!eSE*m#x{X zN#fV9_XhyFfll2tdYey3YIBPp@bSjG%zC*PiE;hp04hgwDOx(D$G|Ao0(;rrkD-rD<{UHg{a%Tki^r%xF8gI`NgpFbDcwTVC7t<+)v=jQ-V z=hAwB_j8-R6OZ-jH4dJuxD@9y?$vRy%;fg>Nd>UU%U(W^lTN|@k2N1-slcyq51b-@ z;5OIctNgo9eFcbPdEXs;g*nrmBIyJH{@^c+AJJ{K{^$&o z``cqkGhE(~J0dQ=0X9Y~r*#R^!txVutlO3XF@~6+DnZfNS3FGrEVN$^6Zc~><)KK_ z(#RJD@I>8{R`2=Vd2h|u6{;COy_v-GM$HW_C~7a_?rmg`gypI1Esw|3 zJSEnIfqBZx`#6CJl%P6-L%(a1=x?{iThJNKlxL(Lz&yo}ATT-e<+1V|)$TN|helv2soa1$!1T+&&dw`0Dz}C*Y?c_@kgl)9d)yw3 z;u$aRQ4I&2b=jj*gqcysVDN$RU(1Bd@4`D~K$Ru-4T-CjBO@U*4%uaumy#VS6dZX! zBNZnqtg2?Lx#1>$JyEUYq&ipc%c8j3@DNkgvP^R!+S6g!@9enO=TF(A7E{`NhSDh5 zXWOdS0Gg6k6c*Hui_INX| zZ}wV+&cCpcC?^b4k>M6H ztr$vI^d$=d3-8K7bCvmln2s3Sk}g)COqk zJLK872IQ^oWr((a-b%67)J)~QVj*zVk;mRIBJs-T=umEF7(;^BW{1hEU1=UO&Y$$i z%pYy$2V!cydzU$ta*4^4`IAs)OJ#d@tkkK_^J@oh=%27?k|U!?f>v3(ufBPsxVEJ>nhUqTe5T2_dHM#HKq!sF1x3ciBRFIvNHR0Ctoqlc) zB*>QZrve-?E2T$8sBksh@gKdAdH)YVBqB6TxXPNVtd%#|?=C<0?MgH+`Q7oLR}5lfm9X(i zC~atPLg4X1`x>?L!NBwViL^z>UWYQziQ+o?U+Kksj@>02glx(oVW+3V$(2KrT_q)@ zDQRAAZ3;oHu6EMJHwd)b1Af`mL@o+}9=A?VFFS{ZKM|I!xpT`|1@Kz$$YxJWL~|{9d{1LZsBF@;-asn z%j!)%je&ZxUWAzhACpBcBe!X0sVC0IhzF20E>!R-Op}sI`NV$QW03XlwRjPxlLi^2 z9w*l-1Rixaw^VEy%YP|;l+aKff%|kOyRjB;3n!l^?dnf$h2`u8b`>IF8CQR}JZ>z=&4`^9hL1BYzlp&o<}gVC#j zwQHvWGHY5!H*Bc=Gfu9)gr~+U(YH+6U^|0OZz}*GES+%u*HU@nieH5(ogvT2A_H?8dz{o1{g8y<4`tx)l;SaeCWhcV;QU{oO?XW3KL z8E)pDxDH{Bxg`nRyRu<7dukwL4v^qW`%ve;I-4E9f=BnV4jFp0?(AN#{r$gpVO-rs zGft$Ts-fH6Z}hi1CyGMN7g_C>0RGl)mdo@e+)n4|$l;y99%OMhbW|vM+hj{hq85ZFzHo?zKIV8cY70r%~EwwA + @@ -709,6 +712,7 @@

Control Chain Device Update

+
Auto saving banks...
@@ -1017,6 +1021,23 @@

File Manager

+ +
+
+

Tone3000

+
+
+ +

Tone3000

+

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

+
+
+ +
diff --git a/html/js/desktop.js b/html/js/desktop.js index 631a98bdd..08e7b8abd 100644 --- a/html/js/desktop.js +++ b/html/js/desktop.js @@ -37,6 +37,8 @@ function Desktop(elements) { pedalboardTrigger: $('
'), fileManagerBox: $('
'), fileManagerBoxTrigger: $('
'), + tone3000Box: $('
'), + tone3000BoxTrigger: $('
'), pedalboardBox: $('
'), pedalboardBoxTrigger: $('
'), bankBox: $('
'), @@ -770,6 +772,8 @@ function Desktop(elements) { elements.bankBoxTrigger) this.fileManagerBox = self.makeFileManagerBox(elements.fileManagerBox, elements.fileManagerBoxTrigger) + this.tone3000Box = self.makeTone3000Box(elements.tone3000Box, + elements.tone3000BoxTrigger) this.getPluginsData = function (uris, callback) { $.ajax({ @@ -1313,6 +1317,7 @@ function Desktop(elements) { elements.bankBoxTrigger.statusTooltip() elements.cloudPluginBoxTrigger.statusTooltip() elements.fileManagerBoxTrigger.statusTooltip() + elements.tone3000BoxTrigger.statusTooltip() this.upgradeWindow = elements.upgradeWindow.upgradeWindow({ icon: elements.upgradeIcon, @@ -1810,6 +1815,14 @@ Desktop.prototype.makeFileManagerBox = function (el, trigger) { }) } +Desktop.prototype.makeTone3000Box = function (el, trigger) { + var self = this + el.tone3000Box({ + trigger: trigger, + windowManager: this.windowManager, + }) +} + Desktop.prototype.reset = function (callback, skipConfirmDialog) { if (this.pedalboardModified && !skipConfirmDialog && !confirm("There are unsaved modifications that will be lost. Are you sure?")) { return diff --git a/html/js/tone3000.js b/html/js/tone3000.js new file mode 100644 index 000000000..89be5dcf1 --- /dev/null +++ b/html/js/tone3000.js @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +JqueryClass('tone3000Box', { + init: function (options) { + var self = $(this) + + options = $.extend({ + isMainWindow: true, + windowName: "Tone3000", + }, options) + + self.data(options) + self.window(options) + }, +}) From 6b2a49a89647c92c2c8b50e94fedd30973acde0f Mon Sep 17 00:00:00 2001 From: Luis Fagundes Date: Fri, 10 Jul 2026 19:55:38 +0000 Subject: [PATCH 10/14] Tone3000: open the catalog via the OAuth PKCE Select flow The tab cannot embed tone3000.com in an iframe: TONE3000's sign-in cookie is SameSite=Lax, so it is never sent on a cross-site iframe subrequest -- the OTP verifies, the cookie comes back, the next request drops it, and the user silently lands on the login screen again, in every browser. Only TONE3000 could change that. A popup is a top-level browsing context, so its cookies are first-party and none of this applies. So the Open button starts TONE3000's OAuth 2.0 + PKCE "Select" flow in a popup sized and placed to cover the panel's content area: mint the verifier/state pair into sessionStorage, open the window synchronously (while we still hold the user activation from the click), then navigate it to the authorize URL, restricted to format=nam since that is what this device's NAM plugin can load. The client id is the OAuth publishable key -- a public value that grants no data access on its own -- configured via MOD_TONE3000_CLIENT_ID and handed to the page through the index template. The popup deliberately survives tab switches (no windowclose handler): closing it whenever the window manager raises another panel would throw away the user's place in the catalog. Only leaving the page closes it. Note crypto.subtle only exists in secure contexts. localhost is one; the real device, on plain http:// over a LAN IP, is not -- a pure-JS sha256 will be needed before this ships to hardware. Co-Authored-By: Claude Fable 5 --- html/css/less/tone3000.less | 14 +++ html/css/main.css | 2 + html/index.html | 6 + html/js/tone3000.js | 213 ++++++++++++++++++++++++++++++++++++ mod/settings.py | 5 + mod/webserver.py | 5 +- 6 files changed, 244 insertions(+), 1 deletion(-) diff --git a/html/css/less/tone3000.less b/html/css/less/tone3000.less index f4fedf0f7..4563b4fe6 100644 --- a/html/css/less/tone3000.less +++ b/html/css/less/tone3000.less @@ -9,6 +9,8 @@ position: absolute; z-index: 2; } +/* Nothing to lay out: the catalog, and everything the user does with it, lives in a + popup floated over this panel. All this pane holds is the way in. */ #tone3000-wrapper { position: absolute; top: 45px; @@ -47,3 +49,15 @@ #tone3000-wrapper .tone3000-hint:last-of-type { margin-bottom: 1.5em; } +#tone3000-browse { + background: #883996; + border: none; + border-radius: 3px; + color: #fff; + cursor: pointer; + font-size: 14px; + padding: .7em 1.4em; +} +#tone3000-browse:hover { + background: #9d47ad; +} diff --git a/html/css/main.css b/html/css/main.css index c8ffd5fd0..e125dd0e1 100644 --- a/html/css/main.css +++ b/html/css/main.css @@ -12,3 +12,5 @@ #tone3000-wrapper .tone3000-hint{font-size:13px;line-height:1.5;color:#999;max-width:34em;margin:0 0 1em} #tone3000-wrapper .tone3000-hint b{color:#ccc;font-weight:normal} #tone3000-wrapper .tone3000-hint:last-of-type{margin-bottom:1.5em} +#tone3000-browse{background:#883996;border:none;border-radius:3px;color:#fff;cursor:pointer;font-size:14px;padding:.7em 1.4em} +#tone3000-browse:hover{background:#9d47ad} diff --git a/html/index.html b/html/index.html index f5563b982..6ffd5a93e 100644 --- a/html/index.html +++ b/html/index.html @@ -66,6 +66,8 @@ PEDALBOARDS_LABS_URL = '{{pedalboards_labs_url}}' CONTROLCHAIN_URL = '{{controlchain_url}}' LV2_PLUGIN_DIR = '{{lv2_plugin_dir}}' + TONE3000_CLIENT_ID = '{{tone3000_client_id}}' + TONE3000_API = '{{tone3000_api}}' VERSION = '{{version}}' BIN_COMPAT = '{{bin_compat}}' PLATFORM = '{{platform}}' @@ -1026,6 +1028,9 @@

File Manager

Tone3000

+

Tone3000

@@ -1034,6 +1039,7 @@

Tone3000

saved to its own folder under NAM Models, where you will find it in the File Manager.

+
diff --git a/html/js/tone3000.js b/html/js/tone3000.js index 89be5dcf1..34e11fdf9 100644 --- a/html/js/tone3000.js +++ b/html/js/tone3000.js @@ -1,6 +1,209 @@ // SPDX-FileCopyrightText: 2012-2023 MOD Audio UG // SPDX-License-Identifier: AGPL-3.0-or-later +/* + * Tone3000 Select flow. + * + * This runs in a popup, not an iframe, and that is not cosmetic. TONE3000's sign-in + * cookie is SameSite=Lax, so it is never sent on a cross-site iframe subrequest: the + * OTP verifies, the cookie comes back, the next request drops it, and the user lands + * on the login screen again -- silently, with no console error, in every browser. + * Only TONE3000 can change that (SameSite=None; Secure; Partitioned, or calling + * requestStorageAccess() from their own page). A popup is a top-level browsing + * context, so its cookies are first-party and none of this applies. + * + * This file only opens that popup and mints the PKCE pair it will need. Everything + * that happens after the user picks a tone -- the token exchange, the API reads, the + * download itself -- belongs to tone3000-callback.html, which TONE3000 redirects the + * popup to. That page is served from our origin, so it reads the pair straight out of + * the sessionStorage written below. + */ + +var TONE3000_STATE_KEY = 't3k_state' +var TONE3000_VERIFIER_KEY = 't3k_code_verifier' +var TONE3000_REDIRECT_PATH = '/tone3000-callback.html' +var TONE3000_POPUP_NAME = 't3k_select' + +var tone3000Popup = null + +function tone3000Base64Url (bytes) { + var str = String.fromCharCode.apply(null, new Uint8Array(bytes)) + return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') +} + +function tone3000RandomBase64Url (nbytes) { + return tone3000Base64Url(crypto.getRandomValues(new Uint8Array(nbytes))) +} + +// S256 PKCE challenge. crypto.subtle only exists in a secure context. localhost is +// one; the real device, on plain http:// over a LAN IP, is not. A pure-JS sha256 +// will be needed before this ships to hardware. +function tone3000Sha256Base64Url (input) { + return crypto.subtle.digest('SHA-256', new TextEncoder().encode(input)) + .then(tone3000Base64Url) +} + +/* Build the authorize URL, stashing the PKCE verifier and CSRF state in OUR + sessionStorage. The popup gets only a snapshot of it, taken at window.open() time, + which is before these are written -- but the callback page is same-origin with us + and reads them back live through window.opener. */ +function tone3000BuildAuthorizeUrl () { + var verifier = tone3000RandomBase64Url(32) + var state = tone3000RandomBase64Url(16) + + return tone3000Sha256Base64Url(verifier).then(function (challenge) { + sessionStorage.setItem(TONE3000_VERIFIER_KEY, verifier) + sessionStorage.setItem(TONE3000_STATE_KEY, state) + + var params = { + client_id: TONE3000_CLIENT_ID, + redirect_uri: window.location.origin + TONE3000_REDIRECT_PATH, + response_type: 'code', + code_challenge: challenge, + code_challenge_method: 'S256', + state: state, + prompt: 'select_tone', + // The NAM plugin is what consumes these files; don't offer the user + // formats this device cannot load. + format: 'nam', + } + + var qs = [] + for (var key in params) { + qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])) + } + return TONE3000_API + '/api/v1/oauth/authorize?' + qs.join('&') + }) +} + +// Headroom for the popup's own title bar, plus whatever the desktop reserves at the +// bottom of the screen. Tuned by eye; nothing can measure it before the popup exists. +var TONE3000_POPUP_CHROME = 165 + +// What `outerHeight - innerHeight` misses. Chrome under-reports the height of its own +// toolbars by about this much; the true value cannot be recovered from anything a page +// can read. Unused where mozInnerScreenY exists, which needs no estimate at all. +var TONE3000_CHROME_UNDERREPORT = 35 + +/* Screen y of the top of our viewport. + * + * Firefox says so exactly. Everyone else gets `screenY` (the window frame) plus a guess + * at the chrome above the viewport, and the guess runs short -- see the constant. */ +function tone3000ViewportTop () { + if (window.mozInnerScreenY !== undefined) { + return window.mozInnerScreenY + } + return window.screenY + (window.outerHeight - window.innerHeight) + TONE3000_CHROME_UNDERREPORT +} + +/* Cover the panel's content area, starting below our own title bar rather than on top + * of it. #tone3000-wrapper *is* that area, so measure it instead of guessing -- move + * the bar in CSS and the popup follows. */ +function tone3000PopupBounds () { + var wrapper = $('#tone3000-wrapper')[0] + + return { + left: Math.round(window.screenX), + top: Math.round(tone3000ViewportTop() + wrapper.getBoundingClientRect().top), + width: window.innerWidth, + height: window.innerHeight - TONE3000_POPUP_CHROME, + } +} + +function tone3000PopupFeatures (bounds) { + return 'width=' + bounds.width + + ',height=' + bounds.height + + ',left=' + bounds.left + ',top=' + bounds.top + + ',toolbar=no,menubar=no,location=no,status=no,resizable=yes,scrollbars=yes' +} + +// How long to keep nudging the popup into place, and how often. It is shown within a +// frame or two; the rest is slack. +var TONE3000_PLACE_TRIES = 6 +var TONE3000_PLACE_INTERVAL = 40 + +/* The feature string's left/top are a hint, and a browser drops them when the opener is + * maximized -- width and height survive, position does not. moveTo is the lever left, + * and only here: the popup is still the about:blank we opened, so it is same-origin. + * One navigation later it is tone3000.com and moveTo throws. + * + * A single move does not take: window.open() returns before the browser has shown the + * window, and the bounds it was created with are applied at show time, over the top of + * anything we set first. So keep moving it for a beat. Where the feature string was + * honoured this is all no-ops. */ +function tone3000PlacePopup (popup, bounds) { + return new Promise(function (resolve) { + var attempts = 0 + function attempt () { + attempts += 1 + try { + popup.moveTo(bounds.left, bounds.top) + } catch (e) { + // Blocked (Firefox's dom.disable_window_move_resize) or already navigated. + resolve() + return + } + if (attempts >= TONE3000_PLACE_TRIES || popup.closed) { + resolve() + return + } + window.setTimeout(attempt, TONE3000_PLACE_INTERVAL) + } + attempt() + }) +} + +function tone3000OpenSelectPopup () { + if (!TONE3000_CLIENT_ID) { + alert('TONE3000 is not configured on this device: MOD_TONE3000_CLIENT_ID is not set.') + return + } + + if (tone3000Popup && !tone3000Popup.closed) { + tone3000Popup.focus() + return + } + + // Open synchronously, while we still hold the user activation from the click. + // Deferring window.open until after the PKCE promise resolves gets it blocked + // as an unsolicited popup. + var bounds = tone3000PopupBounds() + tone3000Popup = window.open('', TONE3000_POPUP_NAME, tone3000PopupFeatures(bounds)) + if (!tone3000Popup) { + alert('Popup blocked -- allow popups for this site and try again.') + return + } + + // Both have to finish before the popup leaves about:blank: the URL is what we + // navigate to, and after navigating we can no longer move it. + var popup = tone3000Popup + Promise.all([ + tone3000BuildAuthorizeUrl(), + tone3000PlacePopup(popup, bounds), + ]).then(function (results) { + popup.location = results[0] + }).catch(function (err) { + popup.close() + alert('Failed to build the TONE3000 authorize URL: ' + err) + }) +} + +function tone3000ClosePopup () { + if (tone3000Popup && !tone3000Popup.closed) { + tone3000Popup.close() + } + tone3000Popup = null + + sessionStorage.removeItem(TONE3000_STATE_KEY) + sessionStorage.removeItem(TONE3000_VERIFIER_KEY) +} + +/* A popup outlives the page that opened it, and the callback page reads the PKCE pair + out of this window -- so a reload or a navigation would strand it on screen with no + opener left to read. pagehide (not unload) is the event that still fires when the + page goes into the bfcache. */ +window.addEventListener('pagehide', tone3000ClosePopup) + JqueryClass('tone3000Box', { init: function (options) { var self = $(this) @@ -11,6 +214,16 @@ JqueryClass('tone3000Box', { }, options) self.data(options) + + self.find('#tone3000-browse').click(function () { + tone3000OpenSelectPopup() + return false + }) + + /* There is deliberately no windowclose handler. It fires whenever the panel hides -- + including when the window manager hides it to raise another panel -- and closing + the popup there would throw away the user's place in the catalog on every trip + through another tab. The popup outlives the panel; only leaving the page closes it. */ self.window(options) }, }) diff --git a/mod/settings.py b/mod/settings.py index 6f9958cbf..2d79368d2 100644 --- a/mod/settings.py +++ b/mod/settings.py @@ -83,6 +83,11 @@ CONTROLCHAIN_HTTP_ADDRESS = os.environ.pop('MOD_CONTROLCHAIN_HTTP_ADDRESS', "https://download.mod.audio/releases/cc-firmware/v3") +# Tone3000 integration. The client id is the OAuth publishable key (t3k_pub_...), +# a public value: it identifies the app, it grants no data access on its own. +TONE3000_CLIENT_ID = os.environ.get('MOD_TONE3000_CLIENT_ID', "") +TONE3000_API = os.environ.get('MOD_TONE3000_API', "https://www.tone3000.com") + MIDI_BEAT_CLOCK_SENDER_URI = "urn:mod:mclk" MIDI_BEAT_CLOCK_SENDER_INSTANCE_ID = 9993 MIDI_BEAT_CLOCK_SENDER_OUTPUT_PORT = "mclk" # This is the LV2 symbol of the plug-ins OutputPort diff --git a/mod/webserver.py b/mod/webserver.py index fe1971f74..f787452ca 100644 --- a/mod/webserver.py +++ b/mod/webserver.py @@ -38,7 +38,8 @@ DEFAULT_ICON_TEMPLATE, DEFAULT_SETTINGS_TEMPLATE, DEFAULT_ICON_IMAGE, DEFAULT_PEDALBOARD, DEFAULT_SNAPSHOT_NAME, DATA_DIR, KEYS_PATH, USER_FILES_DIR, FAVORITES_JSON_FILE, PREFERENCES_JSON_FILE, USER_ID_JSON_FILE, - DEV_HOST, UNTITLED_PEDALBOARD_NAME, MODEL_CPU, MODEL_TYPE, PEDALBOARDS_LABS_HTTP_ADDRESS) + DEV_HOST, UNTITLED_PEDALBOARD_NAME, MODEL_CPU, MODEL_TYPE, PEDALBOARDS_LABS_HTTP_ADDRESS, + TONE3000_CLIENT_ID, TONE3000_API) from mod import ( TextFileFlusher, WINDOWS, @@ -1819,6 +1820,8 @@ def index(self): 'preferences': json.dumps(SESSION.prefs.prefs), 'bufferSize': get_jack_buffer_size(), 'sampleRate': get_jack_sample_rate(), + 'tone3000_client_id': mod_squeeze(TONE3000_CLIENT_ID), + 'tone3000_api': mod_squeeze(TONE3000_API), } return context From 01881e8475d2cff4e70ed2a015cad71e2dbf4d1b Mon Sep 17 00:00:00 2001 From: Luis Fagundes Date: Fri, 10 Jul 2026 19:56:31 +0000 Subject: [PATCH 11/14] Tone3000: download the selected tone into NAM Models Once the user picks a tone, TONE3000 redirects the popup to /tone3000-callback.html with an authorization code. That page is served from mod-ui's own origin, so it is same-origin with the window that opened it and reads the PKCE verifier and CSRF state live through window.opener -- which means the whole tail of the flow can run right there in the popup: verify state, exchange the code, read the tone and its models, and land the files on the device. A tone is a group of models -- the same capture at several sizes and architectures -- so the selection gives a tone_id, not a file. The page reads the tone, pages through its models, keeps the .nam ones, and downloads them one by one with the Bearer token (every TONE3000 data endpoint, the model_url file included, requires it; /api/v1/* sends Access-Control-Allow-Origin: * so this works from our origin and the token never reaches the device). Each file is handed to our /files/upload route, into a per-tone folder under NAM Models named " (<tone id>)", with filenames kept human-readable and deduplicated only when they actually collide. The page mirrors what the user will find afterwards in the File Manager: the tone's folder with one row per model, each row rendered up front and lighting up in place -- downloading, saved, or failed with the reason on hover -- as the chain reaches it. Since the authorization code is single-use, "Continue browsing" mints a fresh authorize URL through the opener and reuses the popup for it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- html/tone3000-callback.html | 418 ++++++++++++++++++++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 html/tone3000-callback.html diff --git a/html/tone3000-callback.html b/html/tone3000-callback.html new file mode 100644 index 000000000..3af70bca5 --- /dev/null +++ b/html/tone3000-callback.html @@ -0,0 +1,418 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Tone3000 download + + + + +
+

TONE3000

+

Getting your tone…

+

Exchanging the authorization code…

+ + + + + + +
+ + + + From 763568339aca54cbf9487c3185a0c6a95e957dcd Mon Sep 17 00:00:00 2001 From: Luis Fagundes Date: Fri, 10 Jul 2026 19:57:05 +0000 Subject: [PATCH 12/14] Tone3000: refresh file dropdowns of plugins on the board, new files on top A plugin's file dropdown is filled once, from the /files/list snapshot taken while its GUI was being built, and nothing ever re-reads it -- so a file downloaded afterwards stays invisible until the plugin is removed and re-added. The backend scan is always live; the staleness is entirely client-side. Give GUI a refreshFileTypesLists(fileType, hoist): re-fetch the lists, re-render the plugin's own icon/settings templates off-DOM, and swap in just the file-list widgets, re-registering each through assignControlFunctionality so the current selection is restored from parameter.value. Going through the template is what makes this work for any plugin -- we never build the option nodes ourselves, so we never assume how the author nested them. Re-rendering the whole icon is not an option: its port elements carry the jsPlumb endpoints the connection manager holds on to, and replacing them would cut every cable into the plugin. The expand-button binding moves into assignFileListExpand so the swapped-in widget gets it re-applied -- a list that was short enough at build time may not be after a download. The callback page hands the just-written paths to every NAM plugin on the board through tone3000RefreshFileLists; each refreshed dropdown lifts those to the front, so the tone the user just downloaded is the first thing in the list instead of buried wherever its name happens to sort. Only for as long as that GUI lives -- a reload takes the list straight from /files/list again, in its own order. Co-Authored-By: Claude Fable 5 --- html/index.html | 4 ++ html/js/modgui.js | 124 ++++++++++++++++++++++++++++++++---- html/js/tone3000.js | 18 ++++++ html/tone3000-callback.html | 6 ++ 4 files changed, 139 insertions(+), 13 deletions(-) diff --git a/html/index.html b/html/index.html index 6ffd5a93e..d2f7755b9 100644 --- a/html/index.html +++ b/html/index.html @@ -1039,6 +1039,10 @@

Tone3000

saved to its own folder under NAM Models, where you will find it in the File Manager.

+

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

diff --git a/html/js/modgui.js b/html/js/modgui.js index 25a8feac4..bb70b92cc 100644 --- a/html/js/modgui.js +++ b/html/js/modgui.js @@ -57,6 +57,25 @@ function loadFileTypesList(parameter, dummy, callback) { }) } +// Only worth expanding once the list outgrows the box. Re-run this whenever the options +// change: a list that was short enough at build time may not be after a download. +function assignFileListExpand(elem) { + var list = elem.find('.mod-enumerated-list') + var button = elem.find('.file-list-btn-expand').off('click') + + if (list.length == 1 && list[0].childElementCount > 5) { + button.show().click(function () { + if (elem.hasClass('expanded')) { + elem.removeClass('expanded') + } else { + elem.addClass('expanded') + } + }) + } else { + button.hide() + } +} + function loadDependencies(gui, effect, dummy, callback) { //source, effect, bundle, callback) { var iconLoaded = true var settingsLoaded = true @@ -523,6 +542,96 @@ function GUI(effect, options) { self.triggerJS({ type: 'change', symbol: symbol, value: value }) } + /* A file dropdown is filled once, from the /files/list snapshot taken while the GUI was + being built, and nothing ever re-reads it -- so a file added afterwards stays invisible + to a plugin already on the board. The backend scan is always live; the staleness is + entirely here. + + Re-render the plugin's own templates and swap in just the file-list widgets. Going + through the template is what makes this work for any plugin: we never build the option + nodes ourselves, so we never assume how the author nested them. Re-rendering the whole + icon is not an option -- its port elements carry the jsPlumb endpoints the connection + manager holds on to, so replacing them would cut every cable into this plugin. */ + this.refreshFileTypesLists = function (fileType, hoist) { + var parameters = [] + for (var i in effect.parameters) { + var parameter = effect.parameters[i] + if (parameter.path && parameter.fileTypes && parameter.fileTypes.indexOf(fileType) >= 0) { + parameters.push(parameter) + } + } + if (!parameters.length) { + return + } + + /* The list arrives sorted by path. Lift the files named in `hoist` to the front, so a + tone the user just downloaded is the first thing in the dropdown instead of buried + wherever its name happened to sort. Only for as long as this GUI lives -- a reload + takes the list straight from /files/list again, in its own order. */ + var reorder = function (files) { + if (!hoist || !hoist.length) { + return files + } + var isNew = function (file) { + return hoist.indexOf(file.fullname) >= 0 + } + return files.filter(isNew).concat(files.filter(function (file) { return !isNew(file) })) + } + + // The templates read effect.parameters, so refresh those, not the self.parameters copies. + var pending = parameters.length + parameters.forEach(function (parameter) { + loadFileTypesList(parameter, false, function () { + parameter.files = reorder(parameter.files) + if (--pending === 0) { + self.swapFileWidgets() + } + }) + }) + } + + this.swapFileWidgets = function () { + var templateData = self.getTemplateData(effect, self.skipNamespace) + var panels = [ + [self.icon, effect.gui.iconTemplate || options.defaultIconTemplate], + [self.settings, effect.gui.settingsTemplate || options.defaultSettingsTemplate], + ] + + panels.forEach(function (panel) { + var live = panel[0] + if (!live) { + return + } + var fresh = $('
').html(Mustache.render(panel[1], templateData)) + + live.find('[mod-widget=custom-select-path]').each(function () { + var old = $(this) + var uri = old.attr('mod-parameter-uri') + var node = fresh.find('[mod-widget=custom-select-path][mod-parameter-uri="' + uri + '"]') + if (node.length !== 1) { + return + } + + var parameter = self.parameters[uri] + parameter.widgets = parameter.widgets.filter(function (widget) { + return widget[0] !== old[0] + }) + + /* assignControlFunctionality reads mod-instance off the element it is given and + only looks downwards, so hand it a stand-in for the panel holding one widget. + It re-registers the widget and restores the selection from parameter.value. */ + var holder = $('
') + if (self.instance) { + holder.attr('mod-instance', self.instance) + } + self.assignControlFunctionality(holder.append(node), false) + + old.replaceWith(node) + assignFileListExpand(node.closest('.mod-file-list')) + }) + }) + } + // lv2 patch messages, mostly used for parameters this.lv2PatchGet = function (uri) { // let the host know about this @@ -804,6 +913,7 @@ function GUI(effect, options) { this.render = function (instance, callback, skipNamespace) { self.instance = instance + self.skipNamespace = skipNamespace var render = function () { self.preRender() @@ -1020,19 +1130,7 @@ function GUI(effect, options) { if (instance && self.effect.parameters.length) { self.settings.find('.mod-file-list').each(function () { - var elem = $(this) - var list = elem.find('.mod-enumerated-list') - if (list.length == 1 && list[0].childElementCount > 5) { - elem.find('.file-list-btn-expand').click(function () { - if (elem.hasClass('expanded')) { - elem.removeClass('expanded') - } else { - elem.addClass('expanded') - } - }) - } else { - elem.find('.file-list-btn-expand').hide() - } + assignFileListExpand($(this)) }) } diff --git a/html/js/tone3000.js b/html/js/tone3000.js index 34e11fdf9..f291e689f 100644 --- a/html/js/tone3000.js +++ b/html/js/tone3000.js @@ -188,6 +188,24 @@ function tone3000OpenSelectPopup () { }) } +/* Called from the popup once a download lands -- it is same-origin with us, so it reaches + this directly. Every plugin already on the board is holding a file list from before the + download, so hand each one a fresh copy, with the files just written at the top of it. + `downloaded` holds the paths the upload route reported writing. */ +function tone3000RefreshFileLists (downloaded) { + if (typeof desktop === 'undefined' || !desktop || !desktop.pedalboard) { + return + } + var plugins = desktop.pedalboard.data('plugins') + for (var instance in plugins) { + var plugin = plugins[instance] + var gui = (plugin && plugin.data) ? plugin.data('gui') : null + if (gui && gui.refreshFileTypesLists) { + gui.refreshFileTypesLists('nammodel', downloaded) + } + } +} + function tone3000ClosePopup () { if (tone3000Popup && !tone3000Popup.closed) { tone3000Popup.close() diff --git a/html/tone3000-callback.html b/html/tone3000-callback.html index 3af70bca5..511ef431d 100644 --- a/html/tone3000-callback.html +++ b/html/tone3000-callback.html @@ -394,6 +394,12 @@

Getting your tone…

if (!okCount) { fail('Could not download this tone: ' + failed[0].err) } else { + // Plugins already on the board cached their file list before this download. + // Hand them the paths just written, so those sort to the top of the dropdown. + if (opener.tone3000RefreshFileLists) { + opener.tone3000RefreshFileLists(results.filter(function (r) { return r.ok }) + .map(function (r) { return r.fullname })) + } if (failed.length) { done('Saved ' + okCount + ' of ' + results.length + ' models to the File Manager.') showSummary(failed.length + ' model' + (failed.length === 1 ? '' : 's') + From 92c96c21013d19baddbec558e0f32001e8bfe905 Mon Sep 17 00:00:00 2001 From: Luis Fagundes Date: Fri, 10 Jul 2026 19:57:55 +0000 Subject: [PATCH 13/14] Tone3000: option to auto-open the catalog when the tab is selected For someone whose whole session is tone-hunting, the intermediate panel is a click that says nothing new. Add an opt-in checkbox that opens the popup straight from selecting the tab: windowopen fires inside the click that selected it, so the user activation window.open needs is still held -- any later and the browser blocks it as unsolicited. The preference is stored browser-side (localStorage, guarded -- it throws outright when storage is disabled, rather than degrading): it is a preference about this browser's popup, not device state, and there is no user-settings route to put it behind. Co-Authored-By: Claude Fable 5 --- html/css/less/tone3000.less | 14 ++++++++++++ html/css/main.css | 3 +++ html/index.html | 4 ++++ html/js/tone3000.js | 43 ++++++++++++++++++++++++++++++++++--- 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/html/css/less/tone3000.less b/html/css/less/tone3000.less index 4563b4fe6..245eb5f33 100644 --- a/html/css/less/tone3000.less +++ b/html/css/less/tone3000.less @@ -49,6 +49,20 @@ #tone3000-wrapper .tone3000-hint:last-of-type { margin-bottom: 1.5em; } +#tone3000-wrapper .tone3000-autoopen { + font-size: 12px; + color: #999; + cursor: pointer; + margin-top: 1.5em; +} +#tone3000-wrapper .tone3000-autoopen input { + margin-right: .5em; + vertical-align: middle; + cursor: pointer; +} +#tone3000-wrapper .tone3000-autoopen:hover { + color: #ccc; +} #tone3000-browse { background: #883996; border: none; diff --git a/html/css/main.css b/html/css/main.css index e125dd0e1..85746bc1b 100644 --- a/html/css/main.css +++ b/html/css/main.css @@ -14,3 +14,6 @@ #tone3000-wrapper .tone3000-hint:last-of-type{margin-bottom:1.5em} #tone3000-browse{background:#883996;border:none;border-radius:3px;color:#fff;cursor:pointer;font-size:14px;padding:.7em 1.4em} #tone3000-browse:hover{background:#9d47ad} +#tone3000-wrapper .tone3000-autoopen{font-size:12px;color:#999;cursor:pointer;margin-top:1.5em} +#tone3000-wrapper .tone3000-autoopen input{margin-right:.5em;vertical-align:middle;cursor:pointer} +#tone3000-wrapper .tone3000-autoopen:hover{color:#ccc} diff --git a/html/index.html b/html/index.html index d2f7755b9..1042630ed 100644 --- a/html/index.html +++ b/html/index.html @@ -1044,6 +1044,10 @@

Tone3000

plugin on the board, so you can load it without hunting for it.

+
diff --git a/html/js/tone3000.js b/html/js/tone3000.js index f291e689f..7319d043a 100644 --- a/html/js/tone3000.js +++ b/html/js/tone3000.js @@ -222,6 +222,27 @@ function tone3000ClosePopup () { page goes into the bfcache. */ window.addEventListener('pagehide', tone3000ClosePopup) +/* Browser-side for now: this is a preference about this browser's popup, not device state, + and there is no user-settings route to put it behind. localStorage throws outright when + storage is disabled, rather than degrading, so every access is guarded. */ +var TONE3000_AUTO_OPEN_KEY = 't3k_auto_open' + +function tone3000AutoOpen () { + try { + return localStorage.getItem(TONE3000_AUTO_OPEN_KEY) === '1' + } catch (e) { + return false + } +} + +function tone3000SetAutoOpen (enabled) { + try { + localStorage.setItem(TONE3000_AUTO_OPEN_KEY, enabled ? '1' : '0') + } catch (e) { + // Nothing to do -- the checkbox still works for this session. + } +} + JqueryClass('tone3000Box', { init: function (options) { var self = $(this) @@ -238,10 +259,26 @@ JqueryClass('tone3000Box', { return false }) - /* There is deliberately no windowclose handler. It fires whenever the panel hides -- + var autoOpen = self.find('#tone3000-autoopen') + autoOpen.prop('checked', tone3000AutoOpen()) + autoOpen.change(function () { + tone3000SetAutoOpen(this.checked) + }) + + /* windowopen fires inside the click that selected the tab, so we still hold the user + activation window.open needs. Opening the popup any later gets it blocked. + + There is deliberately no windowclose handler. It fires whenever the panel hides -- including when the window manager hides it to raise another panel -- and closing - the popup there would throw away the user's place in the catalog on every trip - through another tab. The popup outlives the panel; only leaving the page closes it. */ + the popup there meant every trip through another tab threw away the user's place + in the catalog. The popup outlives the panel; only leaving the page closes it. */ + options.open = function () { + if (tone3000AutoOpen()) { + tone3000OpenSelectPopup() + } + return false + } + self.window(options) }, }) From 6291488d4a1f955fb84c0177a5721b6ec7669daf Mon Sep 17 00:00:00 2001 From: Luis Fagundes Date: Fri, 10 Jul 2026 19:58:23 +0000 Subject: [PATCH 14/14] Add a frontend test suite (node --test + jsdom) The Tone3000 work leans on two pieces of frontend behavior that nothing exercised: the modgui file-list pipeline (including the new in-place refresh, selection restore and hoist-to-top) and the tone3000Box wiring (the auto-open preference, and the popup being focused -- never re-opened or re-navigated -- when the tab is selected again). The app itself is browser-global JS served from html/, so the harness builds a jsdom window, loads the real html/js files into it, and stubs only what jsdom cannot provide (window.open). package.json exists solely to run this suite under Node's built-in test runner; the modgui tests drive a vendored fixture template that reproduces the one property they need from a real plugin icon -- file options nested inside a wrapping element -- so the suite runs in a standalone clone. A jstest workflow runs it in CI next to pylint. Co-Authored-By: Claude Fable 5 --- .github/workflows/jstest.yml | 19 +++ package.json | 12 ++ test/js/README.md | 65 +++++++++ test/js/fixtures/icon-nested.html | 55 ++++++++ test/js/harness.js | 102 ++++++++++++++ test/js/modgui.filelist.test.js | 212 ++++++++++++++++++++++++++++++ test/js/tone3000.autoopen.test.js | 159 ++++++++++++++++++++++ 7 files changed, 624 insertions(+) create mode 100644 .github/workflows/jstest.yml create mode 100644 package.json create mode 100644 test/js/README.md create mode 100644 test/js/fixtures/icon-nested.html create mode 100644 test/js/harness.js create mode 100644 test/js/modgui.filelist.test.js create mode 100644 test/js/tone3000.autoopen.test.js diff --git a/.github/workflows/jstest.yml b/.github/workflows/jstest.yml new file mode 100644 index 000000000..f5fb70777 --- /dev/null +++ b/.github/workflows/jstest.yml @@ -0,0 +1,19 @@ +name: jstest + +on: [push] + +jobs: + jstest: + runs-on: ubuntu-22.04 + name: jstest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Install dependencies + # npm install, not npm ci: the repo ignores package-lock.json (.gitignore), so + # there is no committed lockfile for ci to read. + run: npm install + - name: Run frontend tests + run: npm test diff --git a/package.json b/package.json new file mode 100644 index 000000000..443cc187c --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "mod-ui", + "private": true, + "description": "Frontend tests for mod-ui. The app itself is browser-global JS served from html/; this package exists only to run the test/js/ suite under Node's built-in test runner.", + "scripts": { + "test": "node --test \"test/js/**/*.test.js\"", + "test:watch": "node --test --watch \"test/js/**/*.test.js\"" + }, + "devDependencies": { + "jsdom": "^29.1.1" + } +} diff --git a/test/js/README.md b/test/js/README.md new file mode 100644 index 000000000..d0080f3ec --- /dev/null +++ b/test/js/README.md @@ -0,0 +1,65 @@ +# Frontend tests + +Tests for the browser JavaScript in `html/js/`. The Python backend has its own suite in +`test/` (pytest); this is the JS side. + +## Running + +```bash +npm install # once, pulls in jsdom (the only dependency) +npm test # runs everything under test/js/ +npm run test:watch +``` + +Node's built-in test runner (`node --test`, `node:assert`) — no Jest, no Vitest, no build +step. The only third-party dependency is `jsdom`. + +## How it works + +`html/js/*.js` is not modular. It defines globals and assumes `jQuery`, `Mustache` and a few +page variables already exist, so it cannot be `require`d. Instead `harness.js` stands up a +jsdom window, loads the real vendored libraries, then loads the **real source file under +test** with `window.eval` and drives its now-global functions. The tests exercise the +shipped code, not a copy of it. + +`harness.js` exports: + +- `makeWindow({url, body})` — a jsdom window with jQuery/Mustache loaded and the page globals + (`VERSION`, `baseUrl`, `desktop`, `isSDK`) defined. Returns `{window, $, load}` where + `load(rel)` evals a source file, `rel` being relative to `html/`. +- `captureWindowOptions(ctx)` — grabs the options a `JqueryClass` box hands to + `self.window(...)`, without pulling in the real overlay machinery. +- `stubAjax(ctx, routes)` — an `$.ajax` that answers by URL substring. + +## What this can and cannot test + +jsdom has a DOM but no browser around it. So: + +**In scope** — DOM manipulation and logic. Widget rendering and re-rendering, event wiring, +list ordering, preference storage, the shape of data handed between functions. This is where +the bugs these tests were written for actually lived. + +**Out of scope, stays manual** — anything needing a real browser: + +- **Layout.** `getBoundingClientRect` is all zeros in jsdom, so popup *placement* (the + `mozInnerScreenY` / chrome-height math in `tone3000.js`) cannot be tested here. +- **Windowing.** `window.open` is a stub; there is no real popup, no cross-window focus, no + cookie behaviour. +- **CSS.** No cascade, no computed styles. +- **The network and the Tornado backend.** Backend routes are tested in `test/` (pytest); + the `POST /files/upload` guards and the `fullname` round-trip belong there, not here. + +Keep this boundary honest — a green suite that quietly doesn't cover placement or the backend +is worse than no suite, because it reads as coverage. + +## Files + +- `harness.js` — the rig described above (not itself a test; `*.test.js` are the tests). +- `modgui.filelist.test.js` — `GUI.refreshFileTypesLists` / `swapFileWidgets`: refreshing a + plugin's file dropdown in place, across two templates that nest options differently. Runs + against `fixtures/icon-nested.html` and the shipped `html/resources/settings.html`. +- `fixtures/icon-nested.html` — a plugin-icon template whose file options nest inside a + `.mod-enumerated-list`, the way NAM's icon does. Vendored so the suite needs only the + `mod-ui` checkout, not the sibling `mod-fs/` dev tree. +- `tone3000.autoopen.test.js` — the Tone3000 tab's auto-open preference and the popup's + lifetime relative to the tab. diff --git a/test/js/fixtures/icon-nested.html b/test/js/fixtures/icon-nested.html new file mode 100644 index 000000000..589453cbf --- /dev/null +++ b/test/js/fixtures/icon-nested.html @@ -0,0 +1,55 @@ + +
+
+
+ {{#controls}} +
+
{{name}}
+
+
+ {{/controls}} +
+
+ +
+
+ {{#effect.parameters}} + {{#path}} +
+
+ -- choose a model -- +
+
+ {{#files}} +
{{basename}}
+ {{/files}} +
+
+ {{/path}} + {{/effect.parameters}} +
+
+
+ {{#effect.ports.audio.input}} +
+
+
+ {{/effect.ports.audio.input}} +
+
+ {{#effect.ports.audio.output}} +
+
+
+ {{/effect.ports.audio.output}} +
+
diff --git a/test/js/harness.js b/test/js/harness.js new file mode 100644 index 000000000..34bef67f1 --- /dev/null +++ b/test/js/harness.js @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +/* + * Test rig for the browser-global frontend. + * + * html/js/*.js is not modular -- it defines globals and assumes jQuery, Mustache and a few + * page-scoped variables already exist. So it cannot be require()'d. Instead we stand up a + * jsdom window, load the real vendored libraries into it, then load the real source file + * under test with window.eval, and drive its now-global functions. + * + * What this reaches, it tests for real. What jsdom lacks, it cannot: there is no layout + * (getBoundingClientRect is all zeros), no windowing (window.open is a stub), no network, + * no CSS. Popup *placement*, cross-window behaviour and styling are out of scope here and + * stay manual -- see README.md. + */ + +const fs = require('fs') +const path = require('path') +const { JSDOM } = require('jsdom') + +const HTML = path.resolve(__dirname, '../../html') + +// Vendored libraries the frontend takes for granted, in load order. +const LIBS = [ + 'js/lib/jquery-1.9.1.min.js', + 'js/lib/mustache.js', + 'js/lib/sprintf-0.6.js', +] + +/* + * Build a fresh window with jQuery + Mustache loaded and the globals the frontend expects + * to find on the page. Returns { window, $, load } where load(relPath) evals a real source + * file (path relative to html/) into the window. + * + * opts.url -- document origin, e.g. 'http://localhost:8888/' (default about:blank) + * opts.body -- initial innerHTML + * opts.jqueryUI -- stub draggable/droppable/resizable (icons call these); default true + */ +function makeWindow(opts) { + opts = opts || {} + const dom = new JSDOM('' + (opts.body || '') + '', { + runScripts: 'outside-only', + url: opts.url || 'about:blank', + }) + const window = dom.window + const load = rel => window.eval(fs.readFileSync(path.join(HTML, rel), 'utf8')) + + LIBS.forEach(load) + const $ = window.jQuery + + // Page-scoped globals index.html defines before any js/ file runs. modgui.js reads + // VERSION/baseUrl at GUI-construction time; several files reference desktop/isSDK. + window.eval('var desktop = null; var isSDK = false; var VERSION = 1; var baseUrl = "";') + + // jsdom has no TextEncoder on the window; PKCE code paths want it. + if (!window.TextEncoder) { + window.TextEncoder = require('util').TextEncoder + } + + if (opts.jqueryUI !== false) { + ['draggable', 'droppable', 'resizable'].forEach(m => { $.fn[m] = function () { return this } }) + } + + return { window, $, load } +} + +/* + * Capture what a JqueryClass box hands to self.window(options), without pulling in the real + * window.js overlay machinery. Call before loading a file that defines a *Box widget; the + * returned getter yields the latest options object the box registered. + */ +function captureWindowOptions(ctx) { + let captured = null + ctx.window.JqueryClass = function (name, methods) { + ctx.$.fn[name] = function (o) { return methods.init.apply(this, [o]) } + } + ctx.$.fn.window = function (options) { captured = options; return this } + return () => captured +} + +/* + * An $.ajax stub that answers by URL substring. routes is { substring: responseOrFn }; + * a function receives the ajax options and returns the response passed to success. + * Unmatched URLs invoke opts.error if present. Returns a thenable-ish object like $.ajax. + */ +function stubAjax(ctx, routes) { + ctx.$.ajax = opts => { + const url = opts.url || '' + for (const key in routes) { + if (url.indexOf(key) >= 0) { + const r = routes[key] + opts.success(typeof r === 'function' ? r(opts) : r) + return { done: () => {} } + } + } + if (opts.error) opts.error() + return { done: () => {} } + } +} + +module.exports = { makeWindow, captureWindowOptions, stubAjax, HTML } diff --git a/test/js/modgui.filelist.test.js b/test/js/modgui.filelist.test.js new file mode 100644 index 000000000..327453689 --- /dev/null +++ b/test/js/modgui.filelist.test.js @@ -0,0 +1,212 @@ +// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +/* + * GUI.refreshFileTypesLists / swapFileWidgets -- refreshing a plugin's file dropdown in + * place after a download, without rebuilding the icon (which would cut its jsPlumb cables). + * + * Driven against the REAL html/js/modgui.js, rendered over two templates that nest the + * option nodes differently: html/resources/settings.html (options are direct children of + * the widget) and fixtures/icon-nested.html (options nested inside .mod-enumerated-list, + * the shape NAM's icon uses). The point of the swap is to work for both without assuming + * either. + */ + +const { test, before } = require('node:test') +const assert = require('node:assert') +const fs = require('fs') +const path = require('path') +const { makeWindow, stubAjax, HTML } = require('./harness') + +// The icon template nests the file options inside a .mod-enumerated-list; settings.html +// makes them direct children of the widget. Testing both is the point: the swap must not +// assume either shape. The icon fixture is vendored here (see fixtures/icon-nested.html for +// why); settings.html is the real shipped template, read from the checkout. +const ICON = path.resolve(__dirname, 'fixtures/icon-nested.html') + +const URI = 'http://x#model' + +let ctx, $, iconTemplate, settingsTemplate + +before(() => { + ctx = makeWindow({ url: 'http://localhost:8888/' }) + $ = ctx.$ + ctx.load('js/modgui.js') + iconTemplate = fs.readFileSync(ICON, 'utf8') + settingsTemplate = fs.readFileSync(path.join(HTML, 'resources/settings.html'), 'utf8') +}) + +// Fresh file listing the ajax stub serves; each test sets it. +let listing = [] + +function mkEffect() { + return { + uri: 'http://nam', label: 'NAM', name: 'NAM', renderedVersion: 1, + ports: { audio: { input: [], output: [] }, midi: { input: [], output: [] }, + cv: { input: [], output: [] }, control: { input: [], output: [] } }, + presets: [], parameters: [{ + uri: URI, label: 'Model', type: 'http://lv2plug.in/ns/ext/atom#Path', + fileTypes: ['nam', 'nammodel'], ranges: { default: '' }, value: '', + properties: [], units: {}, writable: true, readable: true, comment: '', + shortName: 'Model', symbol: 'model', + }], + gui: { iconTemplate, settingsTemplate, templateData: {}, javascript: null }, + } +} + +// Build a rendered GUI with an initially empty file list, the way loadDependencies would. +function build() { + // loadDependencies re-fetches the templates over ajax even when effect.gui has them, + // so the stub must answer per-URL, not hand everyone the file list. + stubAjax(ctx, { + '/files/list': () => ({ files: listing.slice() }), + 'iconTemplate': iconTemplate, + 'settingsTemplate': settingsTemplate, + }) + const effect = mkEffect() + const gui = new ctx.window.GUI(effect, { + defaultIconTemplate: '
', defaultSettingsTemplate: '
', + change() {}, patchGet() {}, patchSet() {}, presetLoad() {}, + }) + gui.dependenciesLoaded = true + effect.parameters[0].files = [] + effect.parameters[0].path = true + let icon, settings + gui.render('graph/nam_1', (i, s) => { icon = i; settings = s }, false) + return { gui, icon, settings } +} + +const options = root => root.find('[mod-role=enumeration-option]') +const values = root => options(root).map((i, e) => $(e).attr('mod-parameter-value')).get() +const file = (full, base) => ({ fullname: full, basename: base, filetype: 'nammodel' }) + +test('starts with no options in either panel', () => { + listing = [] + const { icon, settings } = build() + assert.equal(options(icon).length, 0) + assert.equal(options(settings).length, 0) +}) + +test('a download lands options inside each template\'s own container', () => { + listing = [] + const { gui, icon, settings } = build() + listing = [ + file('/u/NAM Models/T (1)/T - standard.nam', 'T - standard.nam'), + file('/u/NAM Models/T (1)/T - lite.nam', 'T - lite.nam'), + ] + gui.refreshFileTypesLists('nammodel') + + const iconWidget = icon.find('[mod-widget=custom-select-path]') + assert.equal(iconWidget.length, 1, 'icon widget still present') + assert.equal(options(icon).length, 2, 'icon has both options') + // The icon nests options one level down; they must be inside the list, not floating. + assert.equal(iconWidget.find('.mod-enumerated-list [mod-role=enumeration-option]').length, 2) + assert.equal(iconWidget.children('[mod-role=enumeration-option]').length, 0) + assert.equal(iconWidget.find('[mod-role=input-parameter-value]').length, 1, 'kept value node') + + const setWidget = settings.find('[mod-widget=custom-select-path]') + assert.equal(options(settings).length, 2, 'settings has both options') + // settings.html makes options direct children of the widget. + assert.equal(setWidget.children('[mod-role=enumeration-option]').length, 2) +}) + +test('parameter.widgets holds exactly the live nodes, no stale entries', () => { + listing = [] + const { gui, icon, settings } = build() + listing = [file('/u/NAM Models/T (1)/T - a.nam', 'T - a.nam')] + gui.refreshFileTypesLists('nammodel') + + const param = gui.parameters[URI] + assert.equal(param.widgets.length, 2, 'one per panel') + assert.ok(param.widgets.every(w => + $.contains(icon[0], w[0]) || $.contains(settings[0], w[0])), 'all in the document') + + // A second refresh must not accumulate widgets. + gui.refreshFileTypesLists('nammodel') + assert.equal(gui.parameters[URI].widgets.length, 2) +}) + +test('the current selection survives a refresh, in both panels', () => { + listing = [ + file('/u/NAM Models/T (1)/T - lite.nam', 'T - lite.nam'), + file('/u/NAM Models/T (1)/T - standard.nam', 'T - standard.nam'), + ] + const { gui, icon, settings } = build() + gui.refreshFileTypesLists('nammodel') + const param = gui.parameters[URI] + param.value = '/u/NAM Models/T (1)/T - lite.nam' + gui.setWritableParameterValue(URI, 'p', param.value, null, true) + + listing.push(file('/u/NAM Models/T (2)/T - nano.nam', 'T - nano.nam')) + gui.refreshFileTypesLists('nammodel') + + assert.equal(options(icon).length, 3) + assert.equal(icon.find('.selected').attr('mod-parameter-value'), param.value) + assert.equal(settings.find('.selected').attr('mod-parameter-value'), param.value) +}) + +test('a value that no longer exists selects nothing, like a fresh build', () => { + listing = [file('/u/a.nam', 'a.nam'), file('/u/b.nam', 'b.nam')] + const { gui, icon } = build() + gui.refreshFileTypesLists('nammodel', ['/u/gone.nam']) + // hoisting a missing file is a no-op; selection was never set, so nothing is selected + gui.parameters[URI].value = '/u/gone.nam' + gui.setWritableParameterValue(URI, 'p', '/u/gone.nam', null, true) + assert.equal(icon.find('.selected').length, 0) +}) + +test('clicking a freshly-swapped option fires exactly one patch_set', () => { + listing = [file('/u/a.nam', 'a.nam'), file('/u/b.nam', 'b.nam')] + const { gui, icon } = build() + gui.refreshFileTypesLists('nammodel') + const sets = [] + gui.lv2PatchSet = (uri, vt, v) => sets.push(v) + icon.find('[mod-parameter-value="/u/b.nam"]').click() + assert.deepEqual(sets, ['/u/b.nam']) +}) + +test('hoisted files lead the list, in both panels', () => { + const LITE = '/u/NAM Models/T (1)/T - lite.nam' + const NANO = '/u/NAM Models/T (2)/T - nano.nam' + listing = [ + file(LITE, 'T - lite.nam'), + file('/u/NAM Models/T (1)/T - standard.nam', 'T - standard.nam'), + file(NANO, 'T - nano.nam'), + ] + const { gui, icon, settings } = build() + gui.refreshFileTypesLists('nammodel', [NANO]) + + assert.equal(values(icon)[0], NANO, 'hoisted file first in icon') + assert.equal(values(settings)[0], NANO, 'hoisted file first in settings') + assert.equal(values(icon).length, 3, 'no options lost') + // non-hoisted keep their /files/list (alphabetical) order after the hoisted ones + assert.equal(values(icon)[1], LITE) + assert.ok(values(icon)[2].endsWith('standard.nam')) +}) + +test('two hoisted files both lead, keeping /files/list order among themselves', () => { + const LITE = '/u/NAM Models/T (1)/T - lite.nam' + const NANO = '/u/NAM Models/T (2)/T - nano.nam' + listing = [ + file(LITE, 'T - lite.nam'), + file('/u/NAM Models/T (1)/T - standard.nam', 'T - standard.nam'), + file(NANO, 'T - nano.nam'), + ] + const { gui, icon } = build() + gui.refreshFileTypesLists('nammodel', [NANO, LITE]) + const lead = values(icon).slice(0, 2) + // passed [NANO, LITE] but the list order is lite-before-nano, and that is what's kept + assert.deepEqual(lead, [LITE, NANO]) + assert.ok(values(icon)[2].endsWith('standard.nam')) +}) + +test('without a hoist argument the list keeps /files/list order', () => { + const LITE = '/u/NAM Models/T (1)/T - lite.nam' + listing = [ + file(LITE, 'T - lite.nam'), + file('/u/NAM Models/T (2)/T - nano.nam', 'T - nano.nam'), + ] + const { gui, icon } = build() + gui.refreshFileTypesLists('nammodel') + assert.equal(values(icon)[0], LITE) +}) diff --git a/test/js/tone3000.autoopen.test.js b/test/js/tone3000.autoopen.test.js new file mode 100644 index 000000000..49f1ecb39 --- /dev/null +++ b/test/js/tone3000.autoopen.test.js @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: 2012-2023 MOD Audio UG +// SPDX-License-Identifier: AGPL-3.0-or-later + +/* + * tone3000Box -- the auto-open preference and the popup's lifetime relative to the tab. + * + * Driven against the REAL html/js/tone3000.js. The popup is a stub (jsdom has no real + * window.open), so this covers the wiring and the preference, not placement. + * + * Two behaviours pinned here, both of which were bugs at some point: + * - the "open automatically" checkbox persists in localStorage and gates windowopen; + * - selecting the tab while the popup is open FOCUSES it, never re-opens or re-navigates + * it -- there is deliberately no windowclose handler, because windowclose fires whenever + * the panel hides, including when another tab is raised. + */ + +const { test, beforeEach } = require('node:test') +const assert = require('node:assert') +const { makeWindow, captureWindowOptions } = require('./harness') + +const KEY = 't3k_auto_open' +const BODY = '
' + + '
' + + '' + + '' + + '
' + +let ctx, $, getOptions +let opened, popups + +// A stand-in popup. `location` is what a re-navigation would write; `focused` counts focus(). +function makePopup() { + const p = { closed: false, focused: 0, location: '', + focus() { this.focused++ }, close() { this.closed = true } } + popups.push(p) + return p +} + +beforeEach(() => { + ctx = makeWindow({ url: 'http://localhost:8888/', body: BODY }) + $ = ctx.$ + getOptions = captureWindowOptions(ctx) + + opened = 0 + popups = [] + ctx.window.open = () => { opened++; return makePopup() } + ctx.window.alert = () => {} + ctx.window.TONE3000_CLIENT_ID = 'pk_test' + ctx.window.TONE3000_API = 'https://www.tone3000.com' + // crypto.subtle.digest never resolves here, so the authorize URL is never built and the + // popup is never navigated -- exactly what lets us assert location stays ''. window.crypto + // is a read-only accessor in jsdom, so define over it rather than assign. + Object.defineProperty(ctx.window, 'crypto', { + value: { getRandomValues: a => a, subtle: { digest: () => new Promise(() => {}) } }, + configurable: true, + }) + ctx.window.btoa = s => Buffer.from(s, 'binary').toString('base64') + + ctx.load('js/tone3000.js') +}) + +const checkbox = () => $('#tone3000-autoopen') +const initBox = () => $('#t3k').tone3000Box({}) +// The popup persists in module state across initBox(); close it so the next +// "does open() open one?" check starts clean, the way a fresh session would. +const closePopups = () => { popups.forEach(p => { p.closed = true }); opened = 0 } + +test('checkbox is off and nothing auto-opens by default', () => { + initBox() + assert.equal(checkbox().prop('checked'), false) + getOptions().open() + assert.equal(opened, 0) +}) + +test('ticking the checkbox persists the preference', () => { + initBox() + checkbox().prop('checked', true).trigger('change') + assert.equal(ctx.window.localStorage.getItem(KEY), '1') +}) + +test('when the preference is on, selecting the tab opens the popup', () => { + ctx.window.localStorage.setItem(KEY, '1') + initBox() + assert.equal(checkbox().prop('checked'), true, 'checkbox restored from storage') + closePopups() + getOptions().open() + assert.equal(opened, 1) +}) + +test('the preference survives a reload (a rebuild of the box)', () => { + ctx.window.localStorage.setItem(KEY, '1') + initBox() + initBox() // reload + assert.equal(checkbox().prop('checked'), true) + closePopups() + getOptions().open() + assert.equal(opened, 1) +}) + +test('unticking clears the preference and stops auto-open', () => { + ctx.window.localStorage.setItem(KEY, '1') + initBox() + checkbox().prop('checked', false).trigger('change') + assert.equal(ctx.window.localStorage.getItem(KEY), '0') + initBox() + assert.equal(checkbox().prop('checked'), false) + closePopups() + getOptions().open() + assert.equal(opened, 0) +}) + +test('open() returns false so it does not swallow the panel-show', () => { + initBox() + closePopups() + assert.equal(getOptions().open(), false) +}) + +test('re-selecting the tab focuses the open popup, never re-opens or re-navigates it', () => { + ctx.window.localStorage.setItem(KEY, '1') + initBox() + closePopups() + popups = [] + + getOptions().open() + assert.equal(opened, 1, 'first selection opens it') + const popup = popups[0] + assert.equal(popup.location, '', 'not navigated (authorize URL never built here)') + + getOptions().open() // leave to another tab, come back + getOptions().open() + assert.equal(opened, 1, 'no second popup') + assert.equal(popup.focused, 2, 'focused each return') + assert.equal(popup.location, '', 'never re-navigated') +}) + +test('there is no windowclose handler -- the popup outlives the panel', () => { + initBox() + assert.equal(getOptions().close, undefined) +}) + +test('once the user closes the popup, the tab opens a fresh one', () => { + ctx.window.localStorage.setItem(KEY, '1') + initBox() + closePopups() + popups = [] + getOptions().open() + popups[0].closed = true + getOptions().open() + assert.equal(opened, 2, 'a closed popup is replaced, not focused') + assert.equal(popups.length, 2) +}) + +test('a localStorage that throws (private mode) does not break the tab', () => { + const real = Object.getOwnPropertyDescriptor(ctx.window, 'localStorage') + Object.defineProperty(ctx.window, 'localStorage', + { get() { throw new Error('denied') }, configurable: true }) + assert.doesNotThrow(() => { initBox(); getOptions().open() }) + Object.defineProperty(ctx.window, 'localStorage', real) +})