diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..8c815de10
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,7 @@
+.git
+.gitignore
+.idea
+**/*.pyc
+**/*.swp
+black-env
+debian
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..b0b9cf043
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,29 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior.
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Installation method**
+How did you install OpenWebRX? (Raspberry Pi SD card image, Debian / Ubuntu packages, Docker image, manually?)
+
+**Versions**
+What version of OpenWebRX are you running? (Check on startup, or see `owrx/version.py`. If a `-dev` version is used, ideally state the commit the issue is appearing on)
+
+**Log messages**
+Are there any relevant messages relating to the bug in the output / log of OpenWebRX? (On most installations, the log should be available using the command `sudo journalctl -u openwebrx`)
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 000000000..ebb33baa8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: General support request or other project-relasted question
+ url: https://groups.io/g/openwebrx
+ about: Request help on the community mailing list
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..c33cd0fdd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: feature
+assignees: ''
+
+---
+
+Before posting a new feature request, please check if a similar idea has already been listed
+* on the issue tracker
+* on the [OpenWebRX github project](https://github.com/users/jketterl/projects/1).
+
+In the latter case, please only proceed if you have additional information about the feature, and please let us know that there's already a card there.
+
+**Feature description**
+Please describe in plain words what functionality you'd like to see in OpenWebRX, and why you think it's useful.
+
+**Target audience**
+Please let us know if you think that this feature is of particular interest for a particular group of users (e.g. hams, SWLs, DXers, ...)
diff --git a/.gitignore b/.gitignore
index 6a211b7e9..8d33574fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
-*.pyc
-*.swp
+**/*.pyc
+**/*.swp
tags
+.idea
+packages
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..a583fd0f2
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,231 @@
+**unreleased**
+- SDR device log messages are now available in the web configuration to simplify troubleshooting
+- Added support for the MSK144 digimode
+- Added support for decoding ADS-B with dump1090
+- Added support for decoding HFDL and VDL2 aircraft communications
+- Added decoding of ISM band transmissions using rtl_433
+- Added support for decoding RDS data on WFM broadcasts using redsea decoder
+- Added decoding for DAB broadcast stations using csdr-eti and dablin
+- Added IPv6 support
+- Added MQTT support
+- New devices supported:
+ - Afedri SDR-Net
+
+**1.2.2**
+- Fixed an over-the-air code injection vulnerability
+
+**1.2.1**
+- FifiSDR support fixed (pipeline formats now line up correctly)
+- Added "Device" input for FifiSDR devices for sound card selection
+
+**1.2.0**
+- Major rewrite of all demodulation components to make use of the new csdr/pycsdr and digiham/pydigiham demodulator
+ modules
+- Preliminary display of M17 callsign information
+- New devices supported:
+ - Blade RF
+
+**1.1.0**
+- Reworked most graphical elements as SVGs for faster loadtimes and crispier display on hi-dpi displays
+- Updated pipelines to match changes in digiham
+- Changed D-Star and NXDN integrations to use new decoders from digiham
+- Added D-Star and NXDN metadata display
+
+**1.0.0**
+- Introduced `squelch_auto_margin` config option that allows configuring the auto squelch level
+- Removed `port` configuration option; `rtltcp_compat` takes the port number with the new connectors
+- Added support for new WSJT-X modes FST4, FST4W (only available with WSJT-X 2.3) and Q65 (only avilable with
+ WSJT-X 2.4)
+- Added support for demodulating M17 digital voice signals using m17-cxx-demod
+- New reporting infrastructure, allowing WSPR and FST4W spots to be sent to wsprnet.org
+- Add some basic filtering capabilities to the map
+- New arguments to the `openwebrx` command-line to facilitate the administration of users (try `openwebrx admin`)
+- Default bandwidth changes:
+ - "WFM" changed to 150kHz
+ - "Packet" (APRS) changed to 12.5kHz
+- Configuration rework:
+ - New: fully web-based configuration interface
+ - System configuration parameters have been moved to a new, separate `openwebrx.conf` file
+ - Remaining parameters are now editable in the web configuration
+ - Existing `config_webrx.py` files will still be read, but changes made in the web configuration will be written to
+ a new storage system
+ - Added upload of avatar and panorama image via web configuration
+- New devices supported:
+ - HPSDR devices (Hermes Lite 2) thanks to @jancona
+ - BBRF103 / RX666 / RX888 devices supported by libsddc
+ - R&S devices using the EB200 or Ammos protocols
+
+**0.20.3**
+- Fix a compatibility issue with python versions <= 3.6
+
+**0.20.2**
+- Fix a security problem that allowed arbitrary commands to be executed on the receiver
+ ([See github issue #215](https://github.com/jketterl/openwebrx/issues/215))
+
+**0.20.1**
+- Remove broken OSM map fallback
+
+**0.20.0**
+- Added the ability to sign multiple keys in a single request, thus enabling multiple users to claim a single receiver
+ on receiverbook.de
+- Fixed file descriptor leaks to prevent "too many open files" errors
+- Add new demodulator chain for FreeDV
+- Added new HD audio streaming mode along with a new WFM demodulator
+- Reworked AGC code for better results in AM, SSB and digital modes
+- Added support for demodulation of "Digital Radio Mondiale" (DRM) broadcast using the "dream" decoder.
+- New default waterfall color scheme
+- Prototype of a continuous automatic waterfall calibration mode
+- New devices supported:
+ - FunCube Dongle Pro+ (`"type": "fcdpp"`)
+ - Support for connections to rtl_tcp (`"type": "rtl_tcp"`)
+
+**0.19.1**
+- Added ability to authenticate receivers with listing sites using "receiver id" tokens
+
+**0.19.0**
+- Fix direwolf connection setup by implementing a retry loop
+- Pass direct sampling mode changes for rtl_sdr_soapy to owrx_connector
+- OSM maps instead of Google when google_maps_api_key is not set (thanks @jquagga)
+- Improved logic to pass parameters to soapy devices.
+ - `rtl_sdr_soapy`: added support for `bias_tee`
+ - `sdrplay`: added support for `bias_tee`, `rf_notch` and `dab_notch`
+ - `airspy`: added support for `bitpack`
+- Added support for Perseus-SDR devices, (thanks @amontefusco)
+- Property System has been rewritten so that defaults on sdr behave as expected
+- Waterfall range auto-adjustment now only takes the center 80% of the spectrum into account, which should work better
+ with SDRs that oversample or have rather flat filter curves towards the spectrum edges
+- Bugfix for negative network usage
+- FiFi SDR: prevent arecord from shutting down after 2GB of data has been sent
+- Added support for bias tee control on rtl_sdr devices
+- All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC
+- `rtl_sdr` type now also supports the `direct_sampling` option
+- Added decoding implementation for for digimode "JS8Call"
+ (requires an installation of [js8call](http://js8call.com/) and
+ [the js8py library](https://github.com/jketterl/js8py))
+- Reorganization of the frontend demodulator code
+- Improve receiver load time by concatenating javascript assets
+- Docker images migrated to Debian slim images; This was necessary to allow the use of function multiversioning in
+ csdr and owrx_connector to allow the images to run on a wider range of CPUs
+- Docker containers have been updated to include the SDRplay driver version 3
+- HackRF support is now based on SoapyHackRF
+- Removed sdr.hu server listing support since the site has been shut down
+- Added support for Radioberry 2 Rasbperry Pi SDR Cape
+
+**0.18.0**
+- Support for SoapyRemote
+
+**2020-02-08**
+- Compression, resampling and filtering in the frontend have been rewritten in javascript, sdr.js has been removed
+- Decoding of Pocsag modulation is now possible
+- Removed the 3D waterfall since it had no real application and required ~1MB of javascript code to be downloaded
+- Improved the frontend handling of the "too many users" scenario
+- PSK63 digimode is now available (same decoding pipeline as PSK31, but with adopted parameters)
+- The frequency can now be manipulated with the mousewheel, which should allow the user to tune more precise. The tuning
+ step size is determined by the digit the mouse cursor is hovering over.
+- Clicking on the frequency now opens an input for direct frequency selection
+- URL hashes have been fixed and improved: They are now updated automatically, so a shared URL will include frequency
+ and demodulator, which allows for improved sharing and linking.
+- New daylight scheduler for background decoding, allows profiles to be selected by local sunrise / sunset times
+- New devices supported:
+ - LimeSDR (`"type": "lime_sdr"`)
+ - PlutoSDR (`"type": "pluto_sdr"`)
+ - RTL_SDR via Soapy (`"type": "rtl_sdr_soapy"`) on special request to allow use of the direct sampling mode
+
+**2020-01-04**
+- The [owrx_connector](https://github.com/jketterl/owrx_connector) is now the default way of communicating with sdr
+ devices. The old sdr types have been replaced, all `_connector` suffixes on the type must be removed!
+- The sources have been refactored, making it a lot easier to add support for other devices
+- SDR device failure handling has been improved, including user feedback
+- New devices supported:
+ - FiFiSDR (`"type": "fifi_sdr"`)
+
+**2019-12-15**
+- wsjt-x updated to 2.1.2
+- The rtl_tcp compatibility mode of the owrx_connector is now configurable using the `rtltcp_compat` flag
+
+**2019-12-10**
+- added support for airspyhf devices (Airspy HF+ / Discovery)
+
+**2019-12-05**
+- explicit device filter for soapy devices for multi-device setups
+
+**2019-12-03**
+- compatibility fixes for safari browsers (ios and mac)
+
+**2019-11-24**
+- There is now a new way to interface with SDR hardware, .
+ They talk directly to the hardware (no rtl_sdr / rx_sdr necessary) and offer I/Q data on a socket, just like nmux
+ did before. They additionally offer a control socket that allows openwebrx to control the SDR parameters directly,
+ without the need for repeated restarts. This allows for quicker profile changes, and also reduces the risk of your
+ SDR hardware from failing during the switchover. See `config_webrx.py` for further information and instructions.
+- Offset tuning using the `lfo_offset` has been reworked in a way that `center_freq` has to be set to the frequency you
+ actually want to listen to. If you're using an `lfo_offset` already, you will probably need to change its sign.
+- `initial_squelch_level` can now be set on each profile.
+- As usual, plenty of fixes and improvements.
+
+**2019-10-27**
+- Part of the frontend code has been reworked
+ - Audio buffer minimums have been completely stripped. As a result, you should get better latency. Unfortunately,
+ this also means there will be some skipping when audio starts.
+ - Now also supports AudioWorklets (for those browser that have it). The Raspberry Pi image has been updated to include
+ https due to the SecureContext requirement.
+ - Mousewheel controls for the receiver sliders
+- Error handling for failed SDR devices
+
+**2019-09-29**
+- One of the most-requested features is finally coming to OpenWebRX: Bookmarks (sometimes also referred to as labels).
+ There's two kinds of bookmarks available:
+ - Serverside bookmarks that are set up by the receiver administrator. Check the file `bookmarks.json` for examples!
+ - Clientside bookmarks which every user can store for themselves. They are stored in the browser's localStorage.
+- Some more bugs in the websocket handling have been fixed.
+
+**2019-09-25**
+- Automatic reporting of spots to [pskreporter](https://pskreporter.info/) is now possible. Please have a look at the
+ configuration on how to set it up.
+- Websocket communication has been overhauled in large parts. It should now be more reliable, and failing connections
+ should now have no impact on other users.
+- Profile scheduling allows to set up band-hopping if you are running background services.
+- APRS now has the ability to show symbols on the map, if a corresponding symbol set has been installed. Check the
+ config!
+- Debug logging has been disabled in a handful of modules, expect vastly reduced output on the shell.
+
+**2019-09-13**
+- New set of APRS-related features
+ - Decode Packet transmissions using [direwolf](https://github.com/wb2osz/direwolf) (1k2 only for now)
+ - APRS packets are mostly decoded and shown both in a new panel and on the map
+ - APRS is also available as a background service
+ - direwolfs I-gate functionality can be enabled, which allows your receiver to work as a receive-only I-gate for the
+ APRS network in the background
+- Demodulation for background services has been optimized to use less total bandwidth, saving CPU
+- More metrics have been added; they can be used together with collectd and its curl_json plugin for now, with some
+ limitations.
+
+**2019-07-21**
+- Latest Features:
+ - More WSJT-X modes have been added, including the new FT4 mode
+ - I started adding a bandplan feature, the first thing visible is the "dial" indicator that brings you right to the
+ dial frequency for digital modes
+ - fixed some bugs in the websocket communication which broke the map
+
+**2019-07-13**
+- Latest Features:
+ - FT8 Integration (using wsjt-x demodulators)
+ - New Map Feature that shows both decoded grid squares from FT8 and Locations decoded from YSF digital voice
+ - New Feature report that will show what functionality is available
+- There's a new Raspbian SD Card image available (see below)
+
+**2019-06-30**
+- I have done some major rework on the openwebrx core, and I am planning to continue adding more features in the near
+ future. Please check this place for updates.
+- My work has not been accepted into the upstream repository, so you will need to chose between my fork and the official
+ version.
+- I have enabled the issue tracker on this project, so feel free to file bugs or suggest enhancements there!
+- This version sports the following new and amazing features:
+ - Support of multiple SDR devices simultaneously
+ - Support for multiple profiles per SDR that allow the user to listen to different frequencies
+ - Support for digital voice decoding
+ - Feature detection that will disable functionality when dependencies are not available (if you're missing the digital
+ buttons, this is probably why)
+- Raspbian SD Card Images and Docker builds available (see below)
+- I am currently working on the feature set for a stable release, but you are more than welcome to test development
+ versions!
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index 79c1a91c4..000000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,15 +0,0 @@
-First of all, thank you for taking the time to contribute to this project!
-
-Before I can accept your contributions, I need a signed copy of the Individual Contributor License Agreement (ICLA) from you, which is available here.
-
-The ICLA is needed because it will allow me to dual license the OpenWebRX project under AGPL and a commercial license.
-I will also apply dual licensing to csdr, but only those parts that are original work (e.g. without the parts enabled by `-DUSE_IMA_ADPCM`; code taken from other projects is clearly separable).
-
-However, even if there is commercial interest in the projects, I promise to keep them as open as possible, keeping my original intention to provide an open-source web-based SDR receiver software to the amateur radio operators and SDR enthusiasts.
-
-This contributor agreement is based on the one of Apache Software Foundation, with some modifications. (You can review differences here).
-When you contribute for the first time, I will send you the ICLA. Replying with only the information requested and the text "I Agree" is sufficient.
-
-Thanks,
-
-Andras, HA7ILM
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
deleted file mode 100644
index 22cca21f2..000000000
--- a/CONTRIBUTORS
+++ /dev/null
@@ -1,5 +0,0 @@
-This is a list of the great people who contributed code to the OpenWebRX repository. (Names are sorted alphabetically.)
-
-Gnoxter
-John Seamons, ZL/KF6VO
-
diff --git a/ICLA.txt b/ICLA.txt
deleted file mode 100644
index d24e4a54d..000000000
--- a/ICLA.txt
+++ /dev/null
@@ -1,128 +0,0 @@
- Individual Contributor License Agreement ("Agreement")
-
-In order to clarify the intellectual property license granted
-with Contributions from any person or entity, Retzler András
-(hereinafter referred to as "Project Owner") must have a
-Contributor License Agreement ("CLA") on file that has
-been signed by each Contributor, indicating agreement to the license
-terms below. This license is for your protection as a Contributor as
-well as the protection of the Project Owner; it does not change your
-rights to use your own Contributions for any other purpose.
-Please read this document carefully before signing and keep a copy
-for your records.
-
- Full name: ______________________________________________________
-
- (optional) Public name: _________________________________________
-
- Mailing Address: ________________________________________________
-
- ________________________________________________
-
- Country: ______________________________________________________
-
- (optional) Telephone: ___________________________________________
-
- E-Mail: ______________________________________________________
-
-You accept and agree to the following terms and conditions for Your
-present and future Contributions submitted to the Project Owner.
-
-Except for the license granted herein to the Project Owner and recipients
-of software distributed by the Project Owner, You reserve all right, title,
-and interest in and to Your Contributions.
-
-1. Definitions.
-
- "You" (or "Your") shall mean the copyright owner or legal entity
- authorized by the copyright owner that is making this Agreement
- with the Project Owner. For legal entities, the entity making a
- Contribution and all other entities that control, are controlled
- by, or are under common control with that entity are considered to
- be a single Contributor. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "Contribution" shall mean any original work of authorship,
- including any modifications or additions to an existing work, that
- is intentionally submitted by You to the Project Owner for inclusion
- in, or documentation of, any of the products owned or managed by
- the Project Owner (the "Work"). For the purposes of this definition,
- "submitted" means any form of electronic, verbal, or written
- communication sent to the Project Owner or its representatives,
- including but not limited to communication on electronic mailing
- lists, source code control systems, and issue tracking systems that
- are managed by, or on behalf of, the Project Owner for the purpose of
- discussing and improving the Work, but excluding communication that
- is conspicuously marked or otherwise designated in writing by You
- as "Not a Contribution."
-
-2. Grant of Copyright License. Subject to the terms and conditions of
- this Agreement, You hereby grant to the Project Owner and to
- recipients of software distributed by the Project Owner a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare derivative works of,
- publicly display, publicly perform, sublicense, and distribute Your
- Contributions and such derivative works.
-
-3. Grant of Patent License. Subject to the terms and conditions of
- this Agreement, You hereby grant to the Project Owner and to
- recipients of software distributed by the Project Owner a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have
- made, use, offer to sell, sell, import, and otherwise transfer the
- Work, where such license applies only to those patent claims
- licensable by You that are necessarily infringed by Your
- Contribution(s) alone or by combination of Your Contribution(s)
- with the Work to which such Contribution(s) was submitted. If any
- entity institutes patent litigation against You or any other entity
- (including a cross-claim or counterclaim in a lawsuit) alleging
- that your Contribution, or the Work to which you have contributed,
- constitutes direct or contributory patent infringement, then any
- patent licenses granted to that entity under this Agreement for
- that Contribution or Work shall terminate as of the date such
- litigation is filed.
-
-4. You represent that you are legally entitled to grant the above
- license. If your employer(s) has rights to intellectual property
- that you create that includes your Contributions, you represent
- that you have received permission to make Contributions on behalf
- of that employer, that your employer has waived such rights for
- your Contributions to the Project Owner, or that your employer has
- executed a separate Corporate CLA with the Project Owner.
-
-5. You represent that each of Your Contributions is Your original
- creation (see section 7 for submissions on behalf of others). You
- represent that Your Contribution submissions include complete
- details of any third-party license or other restriction (including,
- but not limited to, related patents and trademarks) of which you
- are personally aware and which are associated with any part of Your
- Contributions.
-
-6. You are not expected to provide support for Your Contributions,
- except to the extent You desire to provide support. You may provide
- support for free, for a fee, or not at all. Unless required by
- applicable law or agreed to in writing, You provide Your
- Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
- OF ANY KIND, either express or implied, including, without
- limitation, any warranties or conditions of TITLE, NON-
- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
-
-7. Should You wish to submit work that is not Your original creation,
- You may submit it to the Project Owner separately from any
- Contribution, identifying the complete details of its source and of
- any license or other restriction (including, but not limited to,
- related patents, trademarks, and license agreements) of which you
- are personally aware, and conspicuously marking the work as
- "Submitted on behalf of a third-party: [named here]".
-
-8. You agree to notify the Project Owner of any facts or circumstances of
- which you become aware that would make these representations
- inaccurate in any respect.
-
-Please sign: __________________________________ Date: ________________
-
-Text derived from the Apache Individual Contributor License Agreement
-("Agreement") V2.0, available at http://apache.org/licenses/icla.txt
diff --git a/README.md b/README.md
index d308c36f7..f8ed6b87f 100644
--- a/README.md
+++ b/README.md
@@ -1,76 +1,47 @@
OpenWebRX
=========
-[:floppy_disk: Setup guide for Ubuntu](http://blog.sdr.hu/2015/06/30/quick-setup-openwebrx.html) | [:blue_book: Knowledge base on the Wiki](https://github.com/simonyiszk/openwebrx/wiki/) | [:earth_americas: Receivers on SDR.hu](http://sdr.hu/)
-
OpenWebRX is a multi-user SDR receiver software with a web interface.
-![OpenWebRX](http://blog.sdr.hu/images/openwebrx/screenshot.png)
+![OpenWebRX](https://www.openwebrx.de/gfx/openwebrx-screenshot.png)
It has the following features:
-- csdr based demodulators (AM/FM/SSB/CW/BPSK31),
-- filter passband can be set from GUI,
-- waterfall display can be shifted back in time,
-- it extensively uses HTML5 features like WebSocket, Web Audio API, and <canvas>,
-- it works in Google Chrome, Chromium (above version 37) and Mozilla Firefox (above version 28),
-- currently supports RTL-SDR, HackRF, SDRplay, AirSpy and many other devices, see the OpenWebRX Wiki,
-- it has a 3D waterfall display:
-
-![OpenWebRX 3D waterfall](http://blog.sdr.hu/images/openwebrx/screenshot-3d.gif)
-
-**News (2015-08-18)**
-- My BSc. thesis written on OpenWebRX is available here.
-- Several bugs were fixed to improve reliability and stability.
-- OpenWebRX now supports compression of audio and waterfall stream, so the required network uplink bandwidth has been decreased from 2 Mbit/s to about 200 kbit/s per client! (Measured with the default settings. It is also dependent on `fft_size`.)
-- OpenWebRX now uses sdr.js (*libcsdr* compiled to JavaScript) for some client-side DSP tasks.
-- Receivers can now be listed on SDR.hu.
-- License for OpenWebRX is now Affero GPL v3.
-
-**News (2016-02-14)**
-- The DDC in *csdr* has been manually optimized for ARM NEON, so it runs around 3 times faster on the Raspberry Pi 2 than before.
-- Also we use *ncat* instead of *rtl_mus*, and it is 3 times faster in some cases.
-- OpenWebRX now supports URLs like: `http://localhost:8073/#freq=145555000,mod=usb`
-- UI improvements were made, thanks to John Seamons and Gnoxter.
-
-**News (2017-04-04)**
-- *ncat* has been replaced with a custom implementation called *nmux* due to a bug that caused regular crashes on some machines. The *nmux* tool is part of the *csdr* package.
-- Most consumer SDR devices are supported via rx_tools, see the OpenWebRX Wiki on that.
-
-**News (2017-07-12)**
-- OpenWebRX now has a BPSK31 demodulator and a 3D waterfall display.
-
-> When upgrading OpenWebRX, please make sure that you also upgrade *csdr*!
-
-## OpenWebRX servers on SDR.hu
-
-[SDR.hu](http://sdr.hu) is a site which lists the active, public OpenWebRX servers. Your receiver [can also be part of it](http://sdr.hu/openwebrx), if you want.
-
-![sdr.hu](http://blog.sdr.hu/images/openwebrx/screenshot-sdrhu.png)
+- [csdr](https://github.com/jketterl/csdr) based demodulators (AM/FM/SSB/CW/BPSK31/BPSK63)
+- filter passband can be set from GUI
+- it extensively uses HTML5 features like WebSocket, Web Audio API, and Canvas
+- it works in Google Chrome, Chromium and Mozilla Firefox
+- supports a wide range of [SDR hardware](https://github.com/jketterl/openwebrx/wiki/Supported-Hardware#sdr-devices)
+- Multiple SDR devices can be used simultaneously
+- [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF, Pocsag, D-Star, NXDN)
+- [wsjt-x](https://wsjt.sourceforge.io/) based demodulators (FT8, FT4, WSPR, JT65, JT9, FST4,
+ FST4W)
+- [direwolf](https://github.com/wb2osz/direwolf) based demodulation of APRS packets
+- [JS8Call](http://js8call.com/) support
+- [DRM](https://github.com/jketterl/openwebrx/wiki/DRM-demodulator-notes) support
+- [FreeDV](https://github.com/jketterl/openwebrx/wiki/FreeDV-demodulator-notes) support
+- M17 support based on [m17-cxx-demod](https://github.com/mobilinkd/m17-cxx-demod)
## Setup
-OpenWebRX currently requires Linux and python 2.7 to run.
-
-First you will need to install the dependencies:
-
-- libcsdr
-- rtl-sdr
-
-After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server:
-
- python openwebrx.py
+The following methods of setting up a receiver are currently available:
-You can now open the GUI at http://localhost:8073.
+- Raspberry Pi SD card images
+- Debian repository
+- Docker images
+- Manual installation
-Please note that the server is also listening on the following ports (on localhost only):
+Please checkout the [setup guide on the wiki](https://github.com/jketterl/openwebrx/wiki/Setup-Guide) for more details
+on the respective methods.
-- port 4951 for the multi-user I/Q server.
+## Community
-Now the next step is to customize the parameters of your server in `config_webrx.py`.
+If you have trouble setting up or configuring your receiver, you have some great idea you want to see implemented, or
+you just generally want to have some OpenWebRX-related chat, come visit us over on
+[our groups.io group](https://groups.io/g/openwebrx).
-Actually, if you do something cool with OpenWebRX, please drop me a mail:
-*Andras Retzler, HA7ILM <randras@sdr.hu>*
+If you want to hang out, chat, or get in touch directly with the developers, receiver operators or users, feel free to
+drop by in [our Discord server](https://discord.gg/gnE9hPz).
## Usage tips
@@ -80,16 +51,10 @@ The filter envelope can be dragged at its ends and moved around to set the passb
However, if you hold down the shift key, you can drag the center line (BFO) or the whole passband (PBS).
-## Setup tips
-
-If you have any problems installing OpenWebRX, you should check out the Wiki about it, which has a page on the common problems and their solutions.
-
-Sometimes the actual error message is not at the end of the terminal output, you may have to look at the whole output to find it.
-
-If you want to run OpenWebRX on a remote server instead of *localhost*, do not forget to set *server_hostname* in `config_webrx.py`.
-
## Licensing
-OpenWebRX is available under Affero GPL v3 license (summary).
+OpenWebRX is available under Affero GPL v3 license
+([summary](https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0))).
-OpenWebRX is also available under a commercial license on request. Please contact me at the address *<randras@sdr.hu>* for licensing options.
+OpenWebRX is also available under a commercial license on request. Please contact me at the address
+*<randras@sdr.hu>* for licensing options.
diff --git a/bands.json b/bands.json
new file mode 100644
index 000000000..b2b8c4d04
--- /dev/null
+++ b/bands.json
@@ -0,0 +1,391 @@
+[
+ {
+ "name": "2190m",
+ "lower_bound": 135700,
+ "upper_bound": 137800,
+ "frequencies": {
+ "fst4": 136000,
+ "fst4w": 136000,
+ "wspr": 136000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "630m",
+ "lower_bound": 472000,
+ "upper_bound": 479000,
+ "frequencies": {
+ "fst4": 474200,
+ "fst4w": 474200,
+ "wspr": 474200
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "160m",
+ "lower_bound": 1810000,
+ "upper_bound": 2000000,
+ "frequencies": {
+ "bpsk31": 1838000,
+ "ft8": 1840000,
+ "wspr": 1836600,
+ "jt65": 1838000,
+ "jt9": 1839000,
+ "js8": 1842000,
+ "fst4": 1839000,
+ "fst4w": 1836800
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "80m",
+ "lower_bound": 3500000,
+ "upper_bound": 3800000,
+ "frequencies": {
+ "bpsk31": 3580000,
+ "ft8": 3573000,
+ "wspr": 3568600,
+ "jt65": 3570000,
+ "jt9": 3572000,
+ "ft4": [3568000, 3575000],
+ "js8": 3578000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "60m",
+ "lower_bound": 5351500,
+ "upper_bound": 5366500,
+ "frequencies": {
+ "ft8": 5357000,
+ "wspr": [5287200, 5364700]
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "40m",
+ "lower_bound": 7000000,
+ "upper_bound": 7200000,
+ "frequencies": {
+ "bpsk31": 7040000,
+ "ft8": 7074000,
+ "wspr": 7038600,
+ "jt65": 7076000,
+ "jt9": 7078000,
+ "ft4": 7047500,
+ "js8": 7078000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "30m",
+ "lower_bound": 10100000,
+ "upper_bound": 10150000,
+ "frequencies": {
+ "bpsk31": 10141000,
+ "ft8": 10136000,
+ "wspr": 10138700,
+ "jt65": 10138000,
+ "jt9": 10140000,
+ "ft4": 10140000,
+ "js8": 10130000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "20m",
+ "lower_bound": 14000000,
+ "upper_bound": 14350000,
+ "frequencies": {
+ "bpsk31": 14070000,
+ "ft8": 14074000,
+ "wspr": 14095600,
+ "jt65": 14076000,
+ "jt9": 14078000,
+ "ft4": 14080000,
+ "js8": 14078000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "17m",
+ "lower_bound": 18068000,
+ "upper_bound": 18168000,
+ "frequencies": {
+ "bpsk31": 18098000,
+ "ft8": 18100000,
+ "wspr": 18104600,
+ "jt65": 18102000,
+ "jt9": 18104000,
+ "ft4": 18104000,
+ "js8": 18104000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "15m",
+ "lower_bound": 21000000,
+ "upper_bound": 21450000,
+ "frequencies": {
+ "bpsk31": 21070000,
+ "ft8": 21074000,
+ "wspr": 21094600,
+ "jt65": 21076000,
+ "jt9": 21078000,
+ "ft4": 21140000,
+ "js8": 21078000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "12m",
+ "lower_bound": 24890000,
+ "upper_bound": 24990000,
+ "frequencies": {
+ "bpsk31": 24920000,
+ "ft8": 24915000,
+ "wspr": 24924600,
+ "jt65": 24917000,
+ "jt9": 24919000,
+ "ft4": 24919000,
+ "js8": 24922000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "10m",
+ "lower_bound": 28000000,
+ "upper_bound": 29700000,
+ "frequencies": {
+ "bpsk31": [28070000, 28120000],
+ "ft8": 28074000,
+ "wspr": 28124600,
+ "jt65": 28076000,
+ "jt9": 28078000,
+ "ft4": 28180000,
+ "js8": 28078000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "6m",
+ "lower_bound": 50030000,
+ "upper_bound": 51000000,
+ "frequencies": {
+ "bpsk31": 50305000,
+ "ft8": 50313000,
+ "wspr": 50293000,
+ "jt65": 50310000,
+ "jt9": 50312000,
+ "ft4": 50318000,
+ "js8": 50318000,
+ "q65": [50211000, 50275000],
+ "msk144": 50260000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "4m",
+ "lower_bound": 70150000,
+ "upper_bound": 70200000,
+ "frequencies": {
+ "wspr": 70091000,
+ "msk144": 70230000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "2m",
+ "lower_bound": 144000000,
+ "upper_bound": 146000000,
+ "frequencies": {
+ "wspr": 144489000,
+ "ft8": 144174000,
+ "ft4": 144170000,
+ "jt65": 144120000,
+ "packet": 144800000,
+ "q65": 144116000,
+ "msk144": 144360000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "70cm",
+ "lower_bound": 430000000,
+ "upper_bound": 440000000,
+ "frequencies": {
+ "pocsag": 439987500,
+ "q65": 432065000,
+ "msk144": 432360000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "23cm",
+ "lower_bound": 1240000000,
+ "upper_bound": 1300000000,
+ "frequencies": {
+ "q65": 1296065000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "13cm",
+ "lower_bound": 2320000000,
+ "upper_bound": 2450000000,
+ "frequencies": {
+ "q65": [2301065000, 2304065000, 2320065000]
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "9cm",
+ "lower_bound": 3400000000,
+ "upper_bound": 3475000000,
+ "frequencies": {
+ "q65": 3400065000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "6cm",
+ "lower_bound": 5650000000,
+ "upper_bound": 5850000000,
+ "frequencies": {
+ "q65": 5760200000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "3cm",
+ "lower_bound": 10000000000,
+ "upper_bound": 10500000000,
+ "frequencies": {
+ "q65": 10368200000
+ },
+ "tags": ["hamradio"]
+ },
+ {
+ "name": "120m Broadcast",
+ "lower_bound": 2300000,
+ "upper_bound": 2495000,
+ "tags": ["broadcast"]
+ },
+ {
+ "name": "90m Broadcast",
+ "lower_bound": 3200000,
+ "upper_bound": 3400000,
+ "tags": ["broadcast"]
+ },
+ {
+ "name": "75m Broadcast",
+ "lower_bound": 3900000,
+ "upper_bound": 4000000,
+ "tags": ["broadcast"]
+ },
+ {
+ "name": "60m Broadcast",
+ "lower_bound": 4750000,
+ "upper_bound": 4995000,
+ "tags": ["broadcast"]
+ },
+ {
+ "name": "49m Broadcast",
+ "lower_bound": 5900000,
+ "upper_bound": 6200000,
+ "tags": ["broadcast"]
+ },
+ {
+ "name": "41m Broadcast",
+ "lower_bound": 7200000,
+ "upper_bound": 7450000,
+ "tags": ["broadcast"]
+ },
+ {
+ "name": "31m Broadcast",
+ "lower_bound": 9400000,
+ "upper_bound": 9900000,
+ "tags": ["broadcast"]
+ },
+ {
+ "name": "25m Broadcast",
+ "lower_bound": 11600000,
+ "upper_bound": 12100000,
+ "tags": ["broadcast"]
+ },
+ {
+ "name": "22m Broadcast",
+ "lower_bound": 13570000,
+ "upper_bound": 13870000,
+ "tags": ["broadcast"]
+ },
+ {
+ "name": "19m Broadcast",
+ "lower_bound": 15100000,
+ "upper_bound": 15830000,
+ "tags": ["broadcast"]
+ },
+ {
+ "name": "16m Broadcast",
+ "lower_bound": 17480000,
+ "upper_bound": 17900000,
+ "tags": ["broadcast"]
+ },
+ {
+ "name": "15m Broadcast",
+ "lower_bound": 18900000,
+ "upper_bound": 19020000,
+ "tags": ["broadcast"]
+ },
+ {
+ "name": "13m Broadcast",
+ "lower_bound": 21450000,
+ "upper_bound": 21850000,
+ "tags": ["broadcast"]
+ },
+ {
+ "name": "11m Broadcast",
+ "lower_bound": 25670000,
+ "upper_bound": 26100000,
+ "tags": ["broadcast"]
+ },
+ {
+ "name": "FM Broadcast",
+ "lower_bound": 87500000,
+ "upper_bound": 108000000,
+ "tags": ["broadcast"]
+ },
+ {
+ "name": "11m CB",
+ "lower_bound": 26965000,
+ "upper_bound": 27405000,
+ "frequencies": {
+ "js8": 27245000
+ },
+ "tags": ["public"]
+ },
+ {
+ "name": "PMR446",
+ "lower_bound": 446000000,
+ "upper_bound": 446200000,
+ "tags": ["public"]
+ },
+ {
+ "name": "Aeronautical Radionavigation",
+ "lower_bound": 960000000,
+ "upper_bound": 1215000000,
+ "tags": [],
+ "frequencies": {
+ "adsb": 1090000000
+ }
+ },
+ {
+ "name": "ISM-433",
+ "lower_bound": 433050000,
+ "upper_bound": 434790000,
+ "tags": [],
+ "frequencies": {
+ "ism": 433920000
+ }
+ }
+]
diff --git a/config_webrx.py b/config_webrx.py
deleted file mode 100644
index 34e480c6b..000000000
--- a/config_webrx.py
+++ /dev/null
@@ -1,216 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-config_webrx: configuration options for OpenWebRX
-
- This file is part of OpenWebRX,
- an open-source SDR receiver software with a web UI.
- Copyright (c) 2013-2015 by Andras Retzler
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-
- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-
- In addition, as a special exception, the copyright holders
- state that config_rtl.py and config_webrx.py are not part of the
- Corresponding Source defined in GNU AGPL version 3 section 1.
-
- (It means that you do not have to redistribute config_rtl.py and
- config_webrx.py if you make any changes to these two configuration files,
- and use them for running your web service with OpenWebRX.)
-"""
-
-# NOTE: you can find additional information about configuring OpenWebRX in the Wiki:
-# https://github.com/simonyiszk/openwebrx/wiki
-
-# ==== Server settings ====
-web_port=8073
-server_hostname="localhost" # If this contains an incorrect value, the web UI may freeze on load (it can't open websocket)
-max_clients=20
-
-# ==== Web GUI configuration ====
-receiver_name="[Callsign]"
-receiver_location="Budapest, Hungary"
-receiver_qra="JN97ML"
-receiver_asl=200
-receiver_ant="Longwire"
-receiver_device="RTL-SDR"
-receiver_admin="example@example.com"
-receiver_gps=(47.000000,19.000000)
-photo_height=350
-photo_title="Panorama of Budapest from Schönherz Zoltán Dormitory"
-photo_desc="""
-You can add your own background photo and receiver information.
-Receiver is operated by: %[RX_ADMIN]
-Device: %[RX_DEVICE]
-Antenna: %[RX_ANT]
-Website: http://localhost
-"""
-
-# ==== sdr.hu listing ====
-# If you want your ham receiver to be listed publicly on sdr.hu, then take the following steps:
-# 1. Register at: http://sdr.hu/register
-# 2. You will get an unique key by email. Copy it and paste here:
-sdrhu_key = ""
-# 3. Set this setting to True to enable listing:
-sdrhu_public_listing = False
-
-# ==== DSP/RX settings ====
-fft_fps=9
-fft_size=4096 #Should be power of 2
-fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
-
-# samp_rate = 250000
-samp_rate = 2400000
-center_freq = 144250000
-rf_gain = 5 #in dB. For an RTL-SDR, rf_gain=0 will set the tuner to auto gain mode, else it will be in manual gain mode.
-ppm = 0
-
-audio_compression="adpcm" #valid values: "adpcm", "none"
-fft_compression="adpcm" #valid values: "adpcm", "none"
-
-digimodes_enable=True #Decoding digimodes come with higher CPU usage.
-digimodes_fft_size=1024
-
-start_rtl_thread=True
-
-"""
-Note: if you experience audio underruns while CPU usage is 100%, you can:
-- decrease `samp_rate`,
-- set `fft_voverlap_factor` to 0,
-- decrease `fft_fps` and `fft_size`,
-- limit the number of users by decreasing `max_clients`.
-"""
-
-# ==== I/Q sources ====
-# (Uncomment the appropriate by removing # characters at the beginning of the corresponding lines.)
-
-#################################################################################################
-# Is my SDR hardware supported? #
-# Check here: https://github.com/simonyiszk/openwebrx/wiki#guides-for-receiver-hardware-support #
-#################################################################################################
-
-# You can use other SDR hardware as well, by giving your own command that outputs the I/Q samples... Some examples of configuration are available here (default is RTL-SDR):
-
-# >> RTL-SDR via rtl_sdr
-start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm)
-format_conversion="csdr convert_u8_f"
-
-#lna_gain=8
-#rf_amp=1
-#start_rtl_command="hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm, rf_amp=rf_amp, lna_gain=lna_gain)
-#format_conversion="csdr convert_s8_f"
-"""
-To use a HackRF, compile the HackRF host tools from its "stdout" branch:
- git clone https://github.com/mossmann/hackrf/
- cd hackrf
- git fetch
- git checkout origin/stdout
- cd host
- mkdir build
- cd build
- cmake .. -DINSTALL_UDEV_RULES=ON
- make
- sudo make install
-"""
-
-# >> Sound card SDR (needs ALSA)
-# I did not have the chance to properly test it.
-#samp_rate = 96000
-#start_rtl_command="arecord -f S16_LE -r {samp_rate} -c2 -".format(samp_rate=samp_rate)
-#format_conversion="csdr convert_s16_f | csdr gain_ff 30"
-
-# >> /dev/urandom test signal source
-# samp_rate = 2400000
-# start_rtl_command="cat /dev/urandom | (pv -qL `python -c 'print int({samp_rate} * 2.2)'` 2>&1)".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate)
-# format_conversion="csdr convert_u8_f"
-
-# >> Pre-recorded raw I/Q file as signal source
-# You will have to correctly specify: samp_rate, center_freq, format_conversion in order to correctly play an I/Q file.
-#start_rtl_command="(while true; do cat my_iq_file.raw; done) | csdr flowcontrol {sr} 20 ".format(sr=samp_rate*2*1.05)
-#format_conversion="csdr convert_u8_f"
-
-#>> The rx_sdr command works with a variety of SDR harware: RTL-SDR, HackRF, SDRplay, UHD, Airspy, Red Pitaya, audio devices, etc.
-# It will auto-detect your SDR hardware if the following tools are installed:
-# * the vendor provided driver and library,
-# * the vendor-specific SoapySDR wrapper library,
-# * and SoapySDR itself.
-# Check out this article on the OpenWebRX Wiki: https://github.com/simonyiszk/openwebrx/wiki/Using-rx_tools-with-OpenWebRX/
-#start_rtl_command="rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm)
-#format_conversion=""
-
-# >> gr-osmosdr signal source using GNU Radio (follow this guide: https://github.com/simonyiszk/openwebrx/wiki/Using-GrOsmoSDR-as-signal-source)
-#start_rtl_command="cat /tmp/osmocom_fifo"
-#format_conversion=""
-
-# ==== Misc settings ====
-
-shown_center_freq = center_freq #you can change this if you use an upconverter
-
-client_audio_buffer_size = 5
-#increasing client_audio_buffer_size will:
-# - also increase the latency
-# - decrease the chance of audio underruns
-
-start_freq = center_freq
-start_mod = "nfm" #nfm, am, lsb, usb, cw
-
-iq_server_port = 4951 #TCP port for ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default.
-
-#access_log = "~/openwebrx_access.log"
-
-# ==== Color themes ====
-
-#A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels
-
-### default theme by teejez:
-waterfall_colors = "[0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff]"
-waterfall_min_level = -88 #in dB
-waterfall_max_level = -20
-waterfall_auto_level_margin = (5, 40)
-### old theme by HA7ILM:
-#waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
-#waterfall_min_level = -115 #in dB
-#waterfall_max_level = 0
-#waterfall_auto_level_margin = (20, 30)
-##For the old colors, you might also want to set [fft_voverlap_factor] to 0.
-
-#Note: When the auto waterfall level button is clicked, the following happens:
-# [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin[0]]
-# [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin[1]]
-#
-# ___|____________________________________|____________________________________|____________________________________|___> signal power
-# \_waterfall_auto_level_margin[0]_/ |__ current_min_power_level | \_waterfall_auto_level_margin[1]_/
-# current_max_power_level __|
-
-# 3D view settings
-mathbox_waterfall_frequency_resolution = 128 #bins
-mathbox_waterfall_history_length = 10 #seconds
-mathbox_waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
-
-# === Experimental settings ===
-#Warning! The settings below are very experimental.
-csdr_dynamic_bufsize = False # This allows you to change the buffering mode of csdr.
-csdr_print_bufsizes = False # This prints the buffer sizes used for csdr processes.
-csdr_through = False # Setting this True will print out how much data is going into the DSP chains.
-
-nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux.
-
-#Look up external IP address automatically from icanhazip.com, and use it as [server_hostname]
-"""
-print "[openwebrx-config] Detecting external IP address..."
-import urllib2
-server_hostname=urllib2.urlopen("http://icanhazip.com").read()[:-1]
-print "[openwebrx-config] External IP address detected:", server_hostname
-"""
diff --git a/csdr.py b/csdr.py
deleted file mode 100755
index a2fb4902b..000000000
--- a/csdr.py
+++ /dev/null
@@ -1,424 +0,0 @@
-"""
-OpenWebRX csdr plugin: do the signal processing with csdr
-
- This file is part of OpenWebRX,
- an open-source SDR receiver software with a web UI.
- Copyright (c) 2013-2015 by Andras Retzler
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-
-"""
-
-import subprocess
-import time
-import os
-import code
-import signal
-import fcntl
-
-class dsp:
-
- def __init__(self):
- self.samp_rate = 250000
- self.output_rate = 11025 #this is default, and cannot be set at the moment
- self.fft_size = 1024
- self.fft_fps = 5
- self.offset_freq = 0
- self.low_cut = -4000
- self.high_cut = 4000
- self.bpf_transition_bw = 320 #Hz, and this is a constant
- self.ddc_transition_bw_rate = 0.15 # of the IF sample rate
- self.running = False
- self.secondary_processes_running = False
- self.audio_compression = "none"
- self.fft_compression = "none"
- self.demodulator = "nfm"
- self.name = "csdr"
- self.format_conversion = "csdr convert_u8_f"
- self.base_bufsize = 512
- self.nc_port = 4951
- self.csdr_dynamic_bufsize = False
- self.csdr_print_bufsizes = False
- self.csdr_through = False
- self.squelch_level = 0
- self.fft_averages = 50
- self.iqtee = False
- self.iqtee2 = False
- self.secondary_demodulator = None
- self.secondary_fft_size = 1024
- self.secondary_process_fft = None
- self.secondary_process_demod = None
- self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "iqtee_pipe", "iqtee2_pipe"]
- self.secondary_pipe_names=["secondary_shift_pipe"]
- self.secondary_offset_freq = 1000
-
- def chain(self,which):
- any_chain_base="nc -v 127.0.0.1 {nc_port} | "
- if self.csdr_dynamic_bufsize: any_chain_base+="csdr setbuf {start_bufsize} | "
- if self.csdr_through: any_chain_base+="csdr through | "
- any_chain_base+=self.format_conversion+(" | " if self.format_conversion!="" else "") ##"csdr flowcontrol {flowcontrol} auto 1.5 10 | "
- if which == "fft":
- fft_chain_base = any_chain_base+"csdr fft_cc {fft_size} {fft_block_size} | " + \
- ("csdr logpower_cf -70 | " if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages} | ") + \
- "csdr fft_exchange_sides_ff {fft_size}"
- if self.fft_compression=="adpcm":
- return fft_chain_base+" | csdr compress_fft_adpcm_f_u8 {fft_size}"
- else:
- return fft_chain_base
- chain_begin=any_chain_base+"csdr shift_addition_cc --fifo {shift_pipe} | csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 1 | "
- if self.secondary_demodulator:
- chain_begin+="csdr tee {iqtee_pipe} | "
- chain_begin+="csdr tee {iqtee2_pipe} | "
- chain_end = ""
- if self.audio_compression=="adpcm":
- chain_end = " | csdr encode_ima_adpcm_i16_u8"
- if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr fastagc_ff 1024 | csdr convert_f_s16"+chain_end
- elif which == "am": return chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end
- elif which == "ssb": return chain_begin + "csdr realpart_cf | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end
-
- def secondary_chain(self, which):
- secondary_chain_base="cat {input_pipe} | "
- if which == "fft":
- return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "")
- elif which == "bpsk31":
- return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \
- "csdr bandpass_fir_fft_cc $(csdr '=-(31.25)/{if_samp_rate}') $(csdr '=(31.25)/{if_samp_rate}') $(csdr '=31.25/{if_samp_rate}') | " + \
- "csdr simple_agc_cc 0.001 0.5 | " + \
- "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \
- "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \
- "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8"
-
- def set_secondary_demodulator(self, what):
- self.secondary_demodulator = what
-
- def secondary_fft_block_size(self):
- return (self.samp_rate/self.decimation)/(self.fft_fps*2) #*2 is there because we do FFT on real signal here
-
- def secondary_decimation(self):
- return 1 #currently unused
-
- def secondary_bpf_cutoff(self):
- if self.secondary_demodulator == "bpsk31":
- return (31.25/2) / self.if_samp_rate()
- return 0
-
- def secondary_bpf_transition_bw(self):
- if self.secondary_demodulator == "bpsk31":
- return (31.25/2) / self.if_samp_rate()
- return 0
-
- def secondary_samples_per_bits(self):
- if self.secondary_demodulator == "bpsk31":
- return int(round(self.if_samp_rate()/31.25))&~3
- return 0
-
- def secondary_bw(self):
- if self.secondary_demodulator == "bpsk31":
- return 31.25
-
- def start_secondary_demodulator(self):
- if(not self.secondary_demodulator): return
- print "[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate()
- secondary_command_fft=self.secondary_chain("fft")
- secondary_command_demod=self.secondary_chain(self.secondary_demodulator)
- self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft)
-
- secondary_command_fft=secondary_command_fft.format( \
- input_pipe=self.iqtee_pipe, \
- secondary_fft_input_size=self.secondary_fft_size, \
- secondary_fft_size=self.secondary_fft_size, \
- secondary_fft_block_size=self.secondary_fft_block_size(), \
- )
- secondary_command_demod=secondary_command_demod.format( \
- input_pipe=self.iqtee2_pipe, \
- secondary_shift_pipe=self.secondary_shift_pipe, \
- secondary_decimation=self.secondary_decimation(), \
- secondary_samples_per_bits=self.secondary_samples_per_bits(), \
- secondary_bpf_cutoff=self.secondary_bpf_cutoff(), \
- secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), \
- if_samp_rate=self.if_samp_rate()
- )
-
- print "[openwebrx-dsp-plugin:csdr] secondary command (fft) =", secondary_command_fft
- print "[openwebrx-dsp-plugin:csdr] secondary command (demod) =", secondary_command_demod
- #code.interact(local=locals())
- my_env=os.environ.copy()
- #if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
- if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1";
- self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env)
- print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)"
- self.secondary_process_demod = subprocess.Popen(secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) #TODO digimodes
- print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)" #TODO digimodes
- self.secondary_processes_running = True
-
- #open control pipes for csdr and send initialization data
- # print "==========> 1"
- if self.secondary_shift_pipe != None: #TODO digimodes
- # print "==========> 2", self.secondary_shift_pipe
- self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes
- # print "==========> 3"
- self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes
- # print "==========> 4"
-
- self.set_pipe_nonblocking(self.secondary_process_demod.stdout)
- self.set_pipe_nonblocking(self.secondary_process_fft.stdout)
-
- def set_secondary_offset_freq(self, value):
- self.secondary_offset_freq=value
- if self.secondary_processes_running:
- self.secondary_shift_pipe_file.write("%g\n"%(-float(self.secondary_offset_freq)/self.if_samp_rate()))
- self.secondary_shift_pipe_file.flush()
-
- def stop_secondary_demodulator(self):
- if self.secondary_processes_running == False: return
- self.try_delete_pipes(self.secondary_pipe_names)
- if self.secondary_process_fft: os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM)
- if self.secondary_process_demod: os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM)
- self.secondary_processes_running = False
-
- def read_secondary_demod(self, size):
- return self.secondary_process_demod.stdout.read(size)
-
- def read_secondary_fft(self, size):
- return self.secondary_process_fft.stdout.read(size)
-
- def get_secondary_demodulator(self):
- return self.secondary_demodulator
-
- def set_secondary_fft_size(self,secondary_fft_size):
- #to change this, restart is required
- self.secondary_fft_size=secondary_fft_size
-
- def set_audio_compression(self,what):
- self.audio_compression = what
-
- def set_fft_compression(self,what):
- self.fft_compression = what
-
- def get_fft_bytes_to_read(self):
- if self.fft_compression=="none": return self.fft_size*4
- if self.fft_compression=="adpcm": return (self.fft_size/2)+(10/2)
-
- def get_secondary_fft_bytes_to_read(self):
- if self.fft_compression=="none": return self.secondary_fft_size*4
- if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2)
-
- def set_samp_rate(self,samp_rate):
- #to change this, restart is required
- self.samp_rate=samp_rate
- self.decimation=1
- while self.samp_rate/(self.decimation+1)>self.output_rate:
- self.decimation+=1
- self.last_decimation=float(self.if_samp_rate())/self.output_rate
-
- def if_samp_rate(self):
- return self.samp_rate/self.decimation
-
- def get_name(self):
- return self.name
-
- def get_output_rate(self):
- return self.output_rate
-
- def set_output_rate(self,output_rate):
- self.output_rate=output_rate
- self.set_samp_rate(self.samp_rate) #as it depends on output_rate
-
- def set_demodulator(self,demodulator):
- #to change this, restart is required
- self.demodulator=demodulator
-
- def get_demodulator(self):
- return self.demodulator
-
- def set_fft_size(self,fft_size):
- #to change this, restart is required
- self.fft_size=fft_size
-
- def set_fft_fps(self,fft_fps):
- #to change this, restart is required
- self.fft_fps=fft_fps
-
- def set_fft_averages(self,fft_averages):
- #to change this, restart is required
- self.fft_averages=fft_averages
-
- def fft_block_size(self):
- if self.fft_averages == 0: return self.samp_rate/self.fft_fps
- else: return self.samp_rate/self.fft_fps/self.fft_averages
-
- def set_format_conversion(self,format_conversion):
- self.format_conversion=format_conversion
-
- def set_offset_freq(self,offset_freq):
- self.offset_freq=offset_freq
- if self.running:
- self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate))
- self.shift_pipe_file.flush()
-
- def set_bpf(self,low_cut,high_cut):
- self.low_cut=low_cut
- self.high_cut=high_cut
- if self.running:
- self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) )
- self.bpf_pipe_file.flush()
-
- def get_bpf(self):
- return [self.low_cut, self.high_cut]
-
- def set_squelch_level(self, squelch_level):
- self.squelch_level=squelch_level
- if self.running:
- self.squelch_pipe_file.write( "%g\n"%(float(self.squelch_level)) )
- self.squelch_pipe_file.flush()
-
- def get_smeter_level(self):
- if self.running:
- line=self.smeter_pipe_file.readline()
- return float(line[:-1])
-
- def mkfifo(self,path):
- try:
- os.unlink(path)
- except:
- pass
- os.mkfifo(path)
-
- def ddc_transition_bw(self):
- return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate))
-
- def try_create_pipes(self, pipe_names, command_base):
- # print "try_create_pipes"
- for pipe_name in pipe_names:
- # print "\t"+pipe_name
- if "{"+pipe_name+"}" in command_base:
- setattr(self, pipe_name, self.pipe_base_path+pipe_name)
- self.mkfifo(getattr(self, pipe_name))
- else:
- setattr(self, pipe_name, None)
-
- def try_delete_pipes(self, pipe_names):
- for pipe_name in pipe_names:
- pipe_path = getattr(self,pipe_name,None)
- if pipe_path:
- try: os.unlink(pipe_path)
- except Exception as e: print "[openwebrx-dsp-plugin:csdr] try_delete_pipes() ::", e
-
- def set_pipe_nonblocking(self, pipe):
- flags = fcntl.fcntl(pipe, fcntl.F_GETFL)
- fcntl.fcntl(pipe, fcntl.F_SETFL, flags | os.O_NONBLOCK)
-
- def start(self):
- command_base=self.chain(self.demodulator)
-
- #create control pipes for csdr
- self.pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self))
- # self.bpf_pipe = self.shift_pipe = self.squelch_pipe = self.smeter_pipe = None
-
- self.try_create_pipes(self.pipe_names, command_base)
-
- # if "{bpf_pipe}" in command_base:
- # self.bpf_pipe=pipe_base_path+"bpf"
- # self.mkfifo(self.bpf_pipe)
- # if "{shift_pipe}" in command_base:
- # self.shift_pipe=pipe_base_path+"shift"
- # self.mkfifo(self.shift_pipe)
- # if "{squelch_pipe}" in command_base:
- # self.squelch_pipe=pipe_base_path+"squelch"
- # self.mkfifo(self.squelch_pipe)
- # if "{smeter_pipe}" in command_base:
- # self.smeter_pipe=pipe_base_path+"smeter"
- # self.mkfifo(self.smeter_pipe)
- # if "{iqtee_pipe}" in command_base:
- # self.iqtee_pipe=pipe_base_path+"iqtee"
- # self.mkfifo(self.iqtee_pipe)
- # if "{iqtee2_pipe}" in command_base:
- # self.iqtee2_pipe=pipe_base_path+"iqtee2"
- # self.mkfifo(self.iqtee2_pipe)
-
- #run the command
- command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, \
- last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages, \
- bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), \
- flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, \
- squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe )
-
- print "[openwebrx-dsp-plugin:csdr] Command =",command
- #code.interact(local=locals())
- my_env=os.environ.copy()
- if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
- if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1";
- self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env)
- self.running = True
-
- #open control pipes for csdr and send initialization data
- if self.bpf_pipe != None:
- self.bpf_pipe_file=open(self.bpf_pipe,"w")
- self.set_bpf(self.low_cut,self.high_cut)
- if self.shift_pipe != None:
- self.shift_pipe_file=open(self.shift_pipe,"w")
- self.set_offset_freq(self.offset_freq)
- if self.squelch_pipe != None:
- self.squelch_pipe_file=open(self.squelch_pipe,"w")
- self.set_squelch_level(self.squelch_level)
- if self.smeter_pipe != None:
- self.smeter_pipe_file=open(self.smeter_pipe,"r")
- self.set_pipe_nonblocking(self.smeter_pipe_file)
-
- self.start_secondary_demodulator()
-
- def read(self,size):
- return self.process.stdout.read(size)
-
- def stop(self):
- os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
- self.stop_secondary_demodulator()
- #if(self.process.poll()!=None):return # returns None while subprocess is running
- #while(self.process.poll()==None):
- # #self.process.kill()
- # print "killproc",os.getpgid(self.process.pid),self.process.pid
- # os.killpg(self.process.pid, signal.SIGTERM)
- #
- # time.sleep(0.1)
-
- self.try_delete_pipes(self.pipe_names)
-
- # if self.bpf_pipe:
- # try: os.unlink(self.bpf_pipe)
- # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.bpf_pipe
- # if self.shift_pipe:
- # try: os.unlink(self.shift_pipe)
- # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.shift_pipe
- # if self.squelch_pipe:
- # try: os.unlink(self.squelch_pipe)
- # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.squelch_pipe
- # if self.smeter_pipe:
- # try: os.unlink(self.smeter_pipe)
- # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.smeter_pipe
- # if self.iqtee_pipe:
- # try: os.unlink(self.iqtee_pipe)
- # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee_pipe
- # if self.iqtee2_pipe:
- # try: os.unlink(self.iqtee2_pipe)
- # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee2_pipe
-
- self.running = False
-
- def restart(self):
- self.stop()
- self.start()
-
- def __del__(self):
- self.stop()
- del(self.process)
diff --git a/csdr/__init__.py b/csdr/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/csdr/chain/__init__.py b/csdr/chain/__init__.py
new file mode 100644
index 000000000..bebe121de
--- /dev/null
+++ b/csdr/chain/__init__.py
@@ -0,0 +1,147 @@
+from csdr.module import Module
+from pycsdr.modules import Buffer
+from pycsdr.types import Format
+from typing import Union, Callable, Optional
+
+
+class Chain(Module):
+ def __init__(self, workers):
+ super().__init__()
+ self.workers = workers
+ for i in range(1, len(self.workers)):
+ self._connect(self.workers[i - 1], self.workers[i])
+
+ def empty(self):
+ return not self.workers
+
+ def _connect(self, w1, w2, buffer: Optional[Buffer] = None) -> None:
+ if buffer is None:
+ buffer = Buffer(w1.getOutputFormat())
+ w1.setWriter(buffer)
+ w2.setReader(buffer.getReader())
+
+ def setReader(self, reader):
+ if self.reader is reader:
+ return
+ super().setReader(reader)
+ if self.workers:
+ self.workers[0].setReader(reader)
+
+ def setWriter(self, writer):
+ if self.writer is writer:
+ return
+ super().setWriter(writer)
+ if self.workers:
+ self.workers[-1].setWriter(writer)
+
+ def indexOf(self, search: Union[Callable, object]) -> int:
+ def searchFn(x):
+ if callable(search):
+ return search(x)
+ else:
+ return x is search
+
+ try:
+ return next(i for i, v in enumerate(self.workers) if searchFn(v))
+ except StopIteration:
+ return -1
+
+ def replace(self, index, newWorker):
+ if index >= len(self.workers):
+ raise IndexError("Index {} does not exist".format(index))
+
+ self.workers[index].stop()
+ self.workers[index] = newWorker
+
+ error = None
+
+ if index == 0:
+ if self.reader is not None:
+ newWorker.setReader(self.reader)
+ else:
+ try:
+ previousWorker = self.workers[index - 1]
+ self._connect(previousWorker, newWorker)
+ except ValueError as e:
+ # store error for later raising, but still attempt the second connection
+ error = e
+
+ if index == len(self.workers) - 1:
+ if self.writer is not None:
+ newWorker.setWriter(self.writer)
+ else:
+ try:
+ nextWorker = self.workers[index + 1]
+ self._connect(newWorker, nextWorker)
+ except ValueError as e:
+ error = e
+
+ if error is not None:
+ raise error
+
+ def append(self, newWorker):
+ previousWorker = None
+ if self.workers:
+ previousWorker = self.workers[-1]
+
+ self.workers.append(newWorker)
+
+ if previousWorker:
+ self._connect(previousWorker, newWorker)
+ elif self.reader is not None:
+ newWorker.setReader(self.reader)
+
+ if self.writer is not None:
+ newWorker.setWriter(self.writer)
+
+ def insert(self, index, newWorker):
+ nextWorker = None
+ previousWorker = None
+ if index < len(self.workers):
+ nextWorker = self.workers[index]
+ if index > 0:
+ previousWorker = self.workers[index - 1]
+
+ self.workers.insert(index, newWorker)
+
+ if nextWorker:
+ self._connect(newWorker, nextWorker)
+ elif self.writer is not None:
+ newWorker.setWriter(self.writer)
+
+ if previousWorker:
+ self._connect(previousWorker, newWorker)
+ elif self.reader is not None:
+ newWorker.setReader(self.reader)
+
+ def remove(self, index):
+ removedWorker = self.workers[index]
+ self.workers.remove(removedWorker)
+ removedWorker.stop()
+
+ if index == 0:
+ if self.reader is not None and len(self.workers):
+ self.workers[0].setReader(self.reader)
+ elif index == len(self.workers):
+ if self.writer is not None:
+ self.workers[-1].setWriter(self.writer)
+ else:
+ previousWorker = self.workers[index - 1]
+ nextWorker = self.workers[index]
+ self._connect(previousWorker, nextWorker)
+
+ def stop(self):
+ for w in self.workers:
+ w.stop()
+
+ def getInputFormat(self) -> Format:
+ if self.workers:
+ return self.workers[0].getInputFormat()
+ else:
+ raise BufferError("getInputFormat on empty chain")
+
+ def getOutputFormat(self) -> Format:
+ if self.workers:
+ return self.workers[-1].getOutputFormat()
+ else:
+ raise BufferError("getOutputFormat on empty chain")
diff --git a/csdr/chain/analog.py b/csdr/chain/analog.py
new file mode 100644
index 000000000..6124665a3
--- /dev/null
+++ b/csdr/chain/analog.py
@@ -0,0 +1,127 @@
+from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, HdAudio, DeemphasisTauChain, \
+ MetaProvider, RdsChain
+from pycsdr.modules import AmDemod, DcBlock, FmDemod, Limit, NfmDeemphasis, Agc, WfmDeemphasis, FractionalDecimator, \
+ RealPart, Writer, Buffer
+from pycsdr.types import Format, AgcProfile
+from csdr.chain.redsea import Redsea
+from typing import Optional
+from owrx.feature import FeatureDetector
+
+
+class Am(BaseDemodulatorChain):
+ def __init__(self):
+ agc = Agc(Format.FLOAT)
+ agc.setProfile(AgcProfile.SLOW)
+ agc.setInitialGain(200)
+ workers = [
+ AmDemod(),
+ DcBlock(),
+ agc,
+ ]
+
+ super().__init__(workers)
+
+
+class NFm(BaseDemodulatorChain):
+ def __init__(self, sampleRate: int):
+ self.sampleRate = sampleRate
+ agc = Agc(Format.FLOAT)
+ agc.setProfile(AgcProfile.SLOW)
+ agc.setMaxGain(3)
+ workers = [
+ FmDemod(),
+ Limit(),
+ NfmDeemphasis(sampleRate),
+ agc,
+ ]
+ super().__init__(workers)
+
+ def setSampleRate(self, sampleRate: int) -> None:
+ if sampleRate == self.sampleRate:
+ return
+ self.sampleRate = sampleRate
+ self.replace(2, NfmDeemphasis(sampleRate))
+
+
+class WFm(BaseDemodulatorChain, FixedIfSampleRateChain, DeemphasisTauChain, HdAudio, MetaProvider, RdsChain):
+ def __init__(self, sampleRate: int, tau: float, rdsRbds: bool):
+ self.sampleRate = sampleRate
+ self.tau = tau
+ self.rdsRbds = rdsRbds
+ self.limit = Limit()
+ # this buffer is used to tap into the raw audio stream for redsea RDS decoding
+ self.metaTapBuffer = Buffer(Format.FLOAT)
+ workers = [
+ FmDemod(),
+ self.limit,
+ FractionalDecimator(Format.FLOAT, 200000.0 / self.sampleRate, prefilter=True),
+ WfmDeemphasis(self.sampleRate, self.tau),
+ ]
+ self.metaChain = None
+ self.metaWriter = None
+ super().__init__(workers)
+
+ def _connect(self, w1, w2, buffer: Optional[Buffer] = None) -> None:
+ if w1 is self.limit:
+ buffer = self.metaTapBuffer
+ super()._connect(w1, w2, buffer)
+
+ def getFixedIfSampleRate(self):
+ return 200000
+
+ def setDeemphasisTau(self, tau: float) -> None:
+ if tau == self.tau:
+ return
+ self.tau = tau
+ self.replace(3, WfmDeemphasis(self.sampleRate, self.tau))
+
+ def setSampleRate(self, sampleRate: int) -> None:
+ if sampleRate == self.sampleRate:
+ return
+ self.sampleRate = sampleRate
+ self.replace(2, FractionalDecimator(Format.FLOAT, 200000.0 / self.sampleRate, prefilter=True))
+ self.replace(3, WfmDeemphasis(self.sampleRate, self.tau))
+
+ def setMetaWriter(self, writer: Writer) -> None:
+ if not FeatureDetector().is_available("redsea"):
+ return
+ if self.metaChain is None:
+ self.metaChain = Redsea(self.getFixedIfSampleRate(), self.rdsRbds)
+ self.metaChain.setReader(self.metaTapBuffer.getReader())
+ self.metaWriter = writer
+ self.metaChain.setWriter(self.metaWriter)
+
+ def stop(self):
+ super().stop()
+ if self.metaChain is not None:
+ self.metaChain.stop()
+ self.metaChain = None
+ self.metaWriter = None
+
+ def setRdsRbds(self, rdsRbds: bool) -> None:
+ self.rdsRbds = rdsRbds
+ if self.metaChain is not None:
+ self.metaChain.stop()
+ self.metaChain = Redsea(self.getFixedIfSampleRate(), self.rdsRbds)
+ self.metaChain.setReader(self.metaTapBuffer.getReader())
+ self.metaChain.setWriter(self.metaWriter)
+
+
+class Ssb(BaseDemodulatorChain):
+ def __init__(self):
+ workers = [
+ RealPart(),
+ Agc(Format.FLOAT),
+ ]
+ super().__init__(workers)
+
+
+class Empty(BaseDemodulatorChain):
+ def __init__(self):
+ super().__init__([])
+
+ def getOutputFormat(self) -> Format:
+ return Format.FLOAT
+
+ def setWriter(self, writer):
+ pass
diff --git a/csdr/chain/clientaudio.py b/csdr/chain/clientaudio.py
new file mode 100644
index 000000000..9fd748b89
--- /dev/null
+++ b/csdr/chain/clientaudio.py
@@ -0,0 +1,72 @@
+from csdr.chain import Chain
+from pycsdr.modules import AudioResampler, Convert, AdpcmEncoder, Limit
+from pycsdr.types import Format
+
+
+class Converter(Chain):
+ def __init__(self, format: Format, inputRate: int, clientRate: int):
+ workers = []
+ if inputRate != clientRate:
+ # we only have an audio resampler for float ATM so if we need to resample, we need to convert
+ if format != Format.FLOAT:
+ workers += [Convert(format, Format.FLOAT)]
+ workers += [AudioResampler(inputRate, clientRate), Limit(), Convert(Format.FLOAT, Format.SHORT)]
+ elif format != Format.SHORT:
+ workers += [Convert(format, Format.SHORT)]
+ super().__init__(workers)
+
+
+class ClientAudioChain(Chain):
+ def __init__(self, format: Format, inputRate: int, clientRate: int, compression: str):
+ self.format = format
+ self.inputRate = inputRate
+ self.clientRate = clientRate
+ workers = []
+ converter = self._buildConverter()
+ if not converter.empty():
+ workers += [converter]
+ if compression == "adpcm":
+ workers += [AdpcmEncoder(sync=True)]
+ super().__init__(workers)
+
+ def _buildConverter(self):
+ return Converter(self.format, self.inputRate, self.clientRate)
+
+ def _updateConverter(self):
+ converter = self._buildConverter()
+ index = self.indexOf(lambda x: isinstance(x, Converter))
+ if converter.empty():
+ if index >= 0:
+ self.remove(index)
+ else:
+ if index >= 0:
+ self.replace(index, converter)
+ else:
+ self.insert(0, converter)
+
+ def setFormat(self, format: Format) -> None:
+ if format == self.format:
+ return
+ self.format = format
+ self._updateConverter()
+
+ def setInputRate(self, inputRate: int) -> None:
+ if inputRate == self.inputRate:
+ return
+ self.inputRate = inputRate
+ self._updateConverter()
+
+ def setClientRate(self, clientRate: int) -> None:
+ if clientRate == self.clientRate:
+ return
+ self.clientRate = clientRate
+ self._updateConverter()
+
+ def setAudioCompression(self, compression: str) -> None:
+ index = self.indexOf(lambda x: isinstance(x, AdpcmEncoder))
+ if compression == "adpcm":
+ if index < 0:
+ self.append(AdpcmEncoder(sync=True))
+ else:
+ if index >= 0:
+ self.remove(index)
diff --git a/csdr/chain/dablin.py b/csdr/chain/dablin.py
new file mode 100644
index 000000000..c5a321167
--- /dev/null
+++ b/csdr/chain/dablin.py
@@ -0,0 +1,107 @@
+from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, \
+ MetaProvider, DabServiceSelector, DialFrequencyReceiver
+from csdr.module import PickleModule
+from csdreti.modules import EtiDecoder
+from owrx.dab.dablin import DablinModule
+from pycsdr.modules import Downmix, Buffer, Shift, Writer
+from pycsdr.types import Format
+from typing import Optional
+from random import random
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class MetaProcessor(PickleModule):
+ def __init__(self, shifter: Shift):
+ self.shifter = shifter
+ self.shift = 0.0
+ self.coarse_increment = -32 / 2048000
+ self.fine_increment = - (1/3) / 2048000
+ # carrier spacing is 1kHz, don't drift further than that.
+ self.max_shift = 1000 / 2048000
+ super().__init__()
+
+ def process(self, data):
+ result = {}
+ for key, value in data.items():
+ if key == "coarse_frequency_shift":
+ if value > 0:
+ self._nudgeShift(random() * self.coarse_increment)
+ else:
+ self._nudgeShift(random() * -self.coarse_increment)
+ elif key == "fine_frequency_shift":
+ if abs(value) > 10:
+ self._nudgeShift(self.fine_increment * value)
+ else:
+ # pass through everything else
+ result[key] = value
+ # don't send out data if there was nothing interesting for the client
+ if not result:
+ return
+ result["mode"] = "DAB"
+ return result
+
+ def _nudgeShift(self, amount):
+ self.shift += amount
+ if self.shift > self.max_shift:
+ self.shift = self.max_shift
+ elif self.shift < -self.max_shift:
+ self.shift = -self.max_shift
+ logger.debug("new shift: %f", self.shift)
+ self.shifter.setRate(self.shift)
+
+ def resetShift(self):
+ logger.debug("resetting shift")
+ self.shift = 0
+ self.shifter.setRate(0)
+
+
+class Dablin(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, MetaProvider, DabServiceSelector, DialFrequencyReceiver):
+ def __init__(self):
+ shift = Shift(0)
+ self.decoder = EtiDecoder()
+
+ metaBuffer = Buffer(Format.CHAR)
+ self.decoder.setMetaWriter(metaBuffer)
+ self.processor = MetaProcessor(shift)
+ self.processor.setReader(metaBuffer.getReader())
+ # use a dummy to start with. it won't run without.
+ # will be replaced by setMetaWriter().
+ self.processor.setWriter(Buffer(Format.CHAR))
+
+ self.dablin = DablinModule()
+
+ workers = [
+ shift,
+ self.decoder,
+ self.dablin,
+ Downmix(Format.FLOAT),
+ ]
+ super().__init__(workers)
+
+ def _connect(self, w1, w2, buffer: Optional[Buffer] = None) -> None:
+ if isinstance(w2, EtiDecoder):
+ # eti decoder needs big chunks of data
+ buffer = Buffer(w1.getOutputFormat(), size=2097152)
+ super()._connect(w1, w2, buffer)
+
+ def getFixedIfSampleRate(self) -> int:
+ return 2048000
+
+ def getFixedAudioRate(self) -> int:
+ return 48000
+
+ def stop(self):
+ self.processor.stop()
+
+ def setMetaWriter(self, writer: Writer) -> None:
+ self.processor.setWriter(writer)
+
+ def setDabServiceId(self, serviceId: int) -> None:
+ self.decoder.setServiceIdFilter([serviceId])
+ self.dablin.setDabServiceId(serviceId)
+
+ def setDialFrequency(self, frequency: int) -> None:
+ self.processor.resetShift()
diff --git a/csdr/chain/demodulator.py b/csdr/chain/demodulator.py
new file mode 100644
index 000000000..7b4506ccd
--- /dev/null
+++ b/csdr/chain/demodulator.py
@@ -0,0 +1,88 @@
+from csdr.chain import Chain
+from abc import ABC, ABCMeta, abstractmethod
+from pycsdr.modules import Writer
+
+
+class FixedAudioRateChain(ABC):
+ @abstractmethod
+ def getFixedAudioRate(self) -> int:
+ pass
+
+
+class FixedIfSampleRateChain(ABC):
+ @abstractmethod
+ def getFixedIfSampleRate(self) -> int:
+ pass
+
+
+class DialFrequencyReceiver(ABC):
+ @abstractmethod
+ def setDialFrequency(self, frequency: int) -> None:
+ pass
+
+
+# marker interface
+class HdAudio:
+ pass
+
+
+class MetaProvider(ABC):
+ @abstractmethod
+ def setMetaWriter(self, writer: Writer) -> None:
+ pass
+
+
+class SlotFilterChain(ABC):
+ @abstractmethod
+ def setSlotFilter(self, filter: int) -> None:
+ pass
+
+
+class SecondarySelectorChain(ABC):
+ def getBandwidth(self) -> float:
+ pass
+
+
+class DeemphasisTauChain(ABC):
+ @abstractmethod
+ def setDeemphasisTau(self, tau: float) -> None:
+ pass
+
+
+class RdsChain(ABC):
+ @abstractmethod
+ def setRdsRbds(self, rdsRbds: bool) -> None:
+ pass
+
+
+class DabServiceSelector(ABC):
+ @abstractmethod
+ def setDabServiceId(self, serviceId: int) -> None:
+ pass
+
+
+class BaseDemodulatorChain(Chain):
+ def supportsSquelch(self) -> bool:
+ return True
+
+ def setSampleRate(self, sampleRate: int) -> None:
+ pass
+
+
+class SecondaryDemodulator(Chain):
+ def supportsSquelch(self) -> bool:
+ return True
+
+ def setSampleRate(self, sampleRate: int) -> None:
+ pass
+
+ def isSecondaryFftShown(self):
+ return True
+
+
+class ServiceDemodulator(SecondaryDemodulator, FixedAudioRateChain, metaclass=ABCMeta):
+ pass
+
+
+class DemodulatorError(Exception):
+ pass
diff --git a/csdr/chain/digiham.py b/csdr/chain/digiham.py
new file mode 100644
index 000000000..ebd282f0b
--- /dev/null
+++ b/csdr/chain/digiham.py
@@ -0,0 +1,143 @@
+from csdr.chain.demodulator import BaseDemodulatorChain, FixedAudioRateChain, FixedIfSampleRateChain, DialFrequencyReceiver, MetaProvider, SlotFilterChain, DemodulatorError, ServiceDemodulator
+from pycsdr.modules import FmDemod, Agc, Writer, Buffer, DcBlock, Lowpass
+from pycsdr.types import Format
+from digiham.modules import DstarDecoder, FskDemodulator, GfskDemodulator, DigitalVoiceFilter, MbeSynthesizer, NarrowRrcFilter, NxdnDecoder, DmrDecoder, WideRrcFilter, YsfDecoder, PocsagDecoder
+from digiham.ambe import Modes, ServerError
+from owrx.meta import MetaParser
+from owrx.pocsag import PocsagParser
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class DigihamChain(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, DialFrequencyReceiver, MetaProvider):
+ def __init__(self, fskDemodulator, decoder, mbeMode, filter=None, codecserver: str = ""):
+ self.decoder = decoder
+ if codecserver is None:
+ codecserver = ""
+ agc = Agc(Format.SHORT)
+ agc.setMaxGain(30)
+ agc.setInitialGain(3)
+ workers = [FmDemod(), DcBlock()]
+ if filter is not None:
+ workers += [filter]
+ try:
+ mbeSynthesizer = MbeSynthesizer(mbeMode, codecserver)
+ except ConnectionError as ce:
+ raise DemodulatorError("Connection to codecserver failed: {}".format(ce))
+ except ServerError as se:
+ raise DemodulatorError("Codecserver error: {}".format(se))
+ except RuntimeError as re:
+ logger.exception("Codecserver error while instantiating MbeSynthesizer:")
+ raise DemodulatorError("Fatal codecserver error. Please check receiver logs.")
+ workers += [
+ fskDemodulator,
+ decoder,
+ mbeSynthesizer,
+ DigitalVoiceFilter(),
+ agc
+ ]
+ self.metaParser = None
+ self.dialFrequency = None
+ super().__init__(workers)
+
+ def getFixedIfSampleRate(self):
+ return 48000
+
+ def getFixedAudioRate(self):
+ return 8000
+
+ def setMetaWriter(self, writer: Writer) -> None:
+ if self.metaParser is None:
+ self.metaParser = MetaParser()
+ buffer = Buffer(Format.CHAR)
+ self.decoder.setMetaWriter(buffer)
+ self.metaParser.setReader(buffer.getReader())
+ if self.dialFrequency is not None:
+ self.metaParser.setDialFrequency(self.dialFrequency)
+ self.metaParser.setWriter(writer)
+
+ def supportsSquelch(self):
+ return False
+
+ def setDialFrequency(self, frequency: int) -> None:
+ self.dialFrequency = frequency
+ if self.metaParser is None:
+ return
+ self.metaParser.setDialFrequency(frequency)
+
+ def stop(self):
+ if self.metaParser is not None:
+ self.metaParser.stop()
+ super().stop()
+
+
+class Dstar(DigihamChain):
+ def __init__(self, codecserver: str = ""):
+ super().__init__(
+ fskDemodulator=FskDemodulator(samplesPerSymbol=10),
+ decoder=DstarDecoder(),
+ mbeMode=Modes.DStarMode,
+ filter=WideRrcFilter(),
+ codecserver=codecserver
+ )
+
+
+class Nxdn(DigihamChain):
+ def __init__(self, codecserver: str = ""):
+ super().__init__(
+ fskDemodulator=GfskDemodulator(samplesPerSymbol=20),
+ decoder=NxdnDecoder(),
+ mbeMode=Modes.NxdnMode,
+ filter=NarrowRrcFilter(),
+ codecserver=codecserver
+ )
+
+
+class Dmr(DigihamChain, SlotFilterChain):
+ def __init__(self, codecserver: str = ""):
+ super().__init__(
+ fskDemodulator=GfskDemodulator(samplesPerSymbol=10),
+ decoder=DmrDecoder(),
+ mbeMode=Modes.DmrMode,
+ filter=WideRrcFilter(),
+ codecserver=codecserver,
+ )
+
+ def setSlotFilter(self, slotFilter: int) -> None:
+ self.decoder.setSlotFilter(slotFilter)
+
+
+class Ysf(DigihamChain):
+ def __init__(self, codecserver: str = ""):
+ super().__init__(
+ fskDemodulator=GfskDemodulator(samplesPerSymbol=10),
+ decoder=YsfDecoder(),
+ mbeMode=Modes.YsfMode,
+ filter=WideRrcFilter(),
+ codecserver=codecserver
+ )
+
+
+class PocsagDemodulator(ServiceDemodulator, DialFrequencyReceiver):
+ def __init__(self):
+ self.parser = PocsagParser()
+ workers = [
+ FmDemod(),
+ DcBlock(),
+ Lowpass(Format.FLOAT, 1200 / self.getFixedAudioRate()),
+ FskDemodulator(samplesPerSymbol=40, invert=True),
+ PocsagDecoder(),
+ self.parser,
+ ]
+ super().__init__(workers)
+
+ def supportsSquelch(self) -> bool:
+ return False
+
+ def getFixedAudioRate(self) -> int:
+ return 48000
+
+ def setDialFrequency(self, frequency: int) -> None:
+ self.parser.setDialFrequency(frequency)
diff --git a/csdr/chain/digimodes.py b/csdr/chain/digimodes.py
new file mode 100644
index 000000000..a84206093
--- /dev/null
+++ b/csdr/chain/digimodes.py
@@ -0,0 +1,120 @@
+from csdr.chain.demodulator import ServiceDemodulator, SecondaryDemodulator, DialFrequencyReceiver, SecondarySelectorChain
+from csdr.module.msk144 import Msk144Module, ParserAdapter
+from owrx.audio.chopper import AudioChopper, AudioChopperParser
+from owrx.aprs.kiss import KissDeframer
+from owrx.aprs import Ax25Parser, AprsParser
+from pycsdr.modules import Convert, FmDemod, Agc, TimingRecovery, DBPskDecoder, VaricodeDecoder, RttyDecoder, BaudotDecoder, Lowpass
+from pycsdr.types import Format
+from owrx.aprs.direwolf import DirewolfModule
+
+
+class AudioChopperDemodulator(ServiceDemodulator, DialFrequencyReceiver):
+ def __init__(self, mode: str, parser: AudioChopperParser):
+ self.chopper = AudioChopper(mode, parser)
+ workers = [Convert(Format.FLOAT, Format.SHORT), self.chopper]
+ super().__init__(workers)
+
+ def getFixedAudioRate(self):
+ return 12000
+
+ def setDialFrequency(self, frequency: int) -> None:
+ self.chopper.setDialFrequency(frequency)
+
+
+class Msk144Demodulator(ServiceDemodulator, DialFrequencyReceiver):
+ def __init__(self):
+ self.parser = ParserAdapter()
+ workers = [
+ Convert(Format.FLOAT, Format.SHORT),
+ Msk144Module(),
+ self.parser,
+ ]
+ super().__init__(workers)
+
+ def getFixedAudioRate(self) -> int:
+ return 12000
+
+ def setDialFrequency(self, frequency: int) -> None:
+ self.parser.setDialFrequency(frequency)
+
+
+class PacketDemodulator(ServiceDemodulator, DialFrequencyReceiver):
+ def __init__(self, service: bool = False):
+ self.parser = AprsParser()
+ workers = [
+ FmDemod(),
+ Convert(Format.FLOAT, Format.SHORT),
+ DirewolfModule(service=service),
+ KissDeframer(),
+ Ax25Parser(),
+ self.parser,
+ ]
+ super().__init__(workers)
+
+ def supportsSquelch(self) -> bool:
+ return False
+
+ def getFixedAudioRate(self) -> int:
+ return 48000
+
+ def setDialFrequency(self, frequency: int) -> None:
+ self.parser.setDialFrequency(frequency)
+
+
+class PskDemodulator(SecondaryDemodulator, SecondarySelectorChain):
+ def __init__(self, baudRate: float):
+ self.baudRate = baudRate
+ # this is an assumption, we will adjust in setSampleRate
+ self.sampleRate = 12000
+ secondary_samples_per_bits = int(round(self.sampleRate / self.baudRate)) & ~3
+ workers = [
+ Agc(Format.COMPLEX_FLOAT),
+ TimingRecovery(Format.COMPLEX_FLOAT, secondary_samples_per_bits, 0.5, 2),
+ DBPskDecoder(),
+ VaricodeDecoder(),
+ ]
+ super().__init__(workers)
+
+ def getBandwidth(self):
+ return self.baudRate
+
+ def setSampleRate(self, sampleRate: int) -> None:
+ if sampleRate == self.sampleRate:
+ return
+ self.sampleRate = sampleRate
+ secondary_samples_per_bits = int(round(self.sampleRate / self.baudRate)) & ~3
+ self.replace(1, TimingRecovery(Format.COMPLEX_FLOAT, secondary_samples_per_bits, 0.5, 2))
+
+
+class RttyDemodulator(SecondaryDemodulator, SecondarySelectorChain):
+ def __init__(self, baudRate, bandWidth, invert=False):
+ self.baudRate = baudRate
+ self.bandWidth = bandWidth
+ self.invert = invert
+ # this is an assumption, we will adjust in setSampleRate
+ self.sampleRate = 12000
+ secondary_samples_per_bit = int(round(self.sampleRate / self.baudRate))
+ cutoff = self.baudRate / self.sampleRate
+ loop_gain = self.sampleRate / self.getBandwidth() / 5
+ workers = [
+ Agc(Format.COMPLEX_FLOAT),
+ FmDemod(),
+ Lowpass(Format.FLOAT, cutoff),
+ TimingRecovery(Format.FLOAT, secondary_samples_per_bit, loop_gain, 10),
+ RttyDecoder(invert),
+ BaudotDecoder(),
+ ]
+ super().__init__(workers)
+
+ def getBandwidth(self) -> float:
+ return self.bandWidth
+
+ def setSampleRate(self, sampleRate: int) -> None:
+ if sampleRate == self.sampleRate:
+ return
+ self.sampleRate = sampleRate
+ secondary_samples_per_bit = int(round(self.sampleRate / self.baudRate))
+ cutoff = self.baudRate / self.sampleRate
+ loop_gain = self.sampleRate / self.getBandwidth() / 5
+ self.replace(2, Lowpass(Format.FLOAT, cutoff))
+ self.replace(3, TimingRecovery(Format.FLOAT, secondary_samples_per_bit, loop_gain, 10))
diff --git a/csdr/chain/drm.py b/csdr/chain/drm.py
new file mode 100644
index 000000000..78b90f0df
--- /dev/null
+++ b/csdr/chain/drm.py
@@ -0,0 +1,23 @@
+from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain
+from pycsdr.modules import Convert, Downmix
+from pycsdr.types import Format
+from csdr.module.drm import DrmModule
+
+
+class Drm(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain):
+ def __init__(self):
+ workers = [
+ Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT),
+ DrmModule(),
+ Downmix(Format.SHORT),
+ ]
+ super().__init__(workers)
+
+ def supportsSquelch(self) -> bool:
+ return False
+
+ def getFixedIfSampleRate(self) -> int:
+ return 48000
+
+ def getFixedAudioRate(self) -> int:
+ return 48000
diff --git a/csdr/chain/dummy.py b/csdr/chain/dummy.py
new file mode 100644
index 000000000..b4e0220b9
--- /dev/null
+++ b/csdr/chain/dummy.py
@@ -0,0 +1,14 @@
+from pycsdr.types import Format
+from csdr.chain import Module
+
+
+class DummyDemodulator(Module):
+ def __init__(self, outputFormat: Format):
+ self.outputFormat = outputFormat
+ super().__init__()
+
+ def getInputFormat(self) -> Format:
+ return Format.COMPLEX_FLOAT
+
+ def getOutputFormat(self) -> Format:
+ return self.outputFormat
diff --git a/csdr/chain/dump1090.py b/csdr/chain/dump1090.py
new file mode 100644
index 000000000..04a294211
--- /dev/null
+++ b/csdr/chain/dump1090.py
@@ -0,0 +1,27 @@
+from pycsdr.modules import Convert
+from pycsdr.types import Format
+from csdr.chain.demodulator import ServiceDemodulator
+from owrx.adsb.dump1090 import Dump1090Module, RawDeframer
+from owrx.adsb.modes import ModeSParser
+
+
+class Dump1090(ServiceDemodulator):
+ def __init__(self):
+ workers = [
+ Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT),
+ Dump1090Module(),
+ RawDeframer(),
+ ModeSParser(),
+ ]
+
+ super().__init__(workers)
+ pass
+
+ def getFixedAudioRate(self) -> int:
+ return 2400000
+
+ def isSecondaryFftShown(self):
+ return False
+
+ def supportsSquelch(self) -> bool:
+ return False
diff --git a/csdr/chain/dumphfdl.py b/csdr/chain/dumphfdl.py
new file mode 100644
index 000000000..413b61500
--- /dev/null
+++ b/csdr/chain/dumphfdl.py
@@ -0,0 +1,16 @@
+from csdr.chain.demodulator import ServiceDemodulator
+from owrx.hfdl.dumphfdl import DumpHFDLModule, HFDLMessageParser
+
+
+class DumpHFDL(ServiceDemodulator):
+ def __init__(self):
+ super().__init__([
+ DumpHFDLModule(),
+ HFDLMessageParser(),
+ ])
+
+ def getFixedAudioRate(self) -> int:
+ return 12000
+
+ def supportsSquelch(self) -> bool:
+ return False
diff --git a/csdr/chain/dumpvdl2.py b/csdr/chain/dumpvdl2.py
new file mode 100644
index 000000000..cae271f51
--- /dev/null
+++ b/csdr/chain/dumpvdl2.py
@@ -0,0 +1,19 @@
+from csdr.chain.demodulator import ServiceDemodulator
+from owrx.vdl2.dumpvdl2 import DumpVDL2Module, VDL2MessageParser
+from pycsdr.modules import Convert
+from pycsdr.types import Format
+
+
+class DumpVDL2(ServiceDemodulator):
+ def __init__(self):
+ super().__init__([
+ Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT),
+ DumpVDL2Module(),
+ VDL2MessageParser(),
+ ])
+
+ def getFixedAudioRate(self) -> int:
+ return 105000
+
+ def supportsSquelch(self) -> bool:
+ return False
diff --git a/csdr/chain/fft.py b/csdr/chain/fft.py
new file mode 100644
index 000000000..c782ca0d8
--- /dev/null
+++ b/csdr/chain/fft.py
@@ -0,0 +1,96 @@
+from csdr.chain import Chain
+from pycsdr.modules import Fft, LogPower, LogAveragePower, FftSwap, FftAdpcm
+
+
+class FftAverager(Chain):
+ def __init__(self, fft_size, fft_averages):
+ self.fftSize = fft_size
+ self.fftAverages = fft_averages
+ workers = [self._getWorker()]
+ super().__init__(workers)
+
+ def setFftAverages(self, fft_averages):
+ if self.fftAverages == fft_averages:
+ return
+ self.fftAverages = fft_averages
+ self.replace(0, self._getWorker())
+
+ def _getWorker(self):
+ if self.fftAverages == 0:
+ return LogPower(add_db=-70)
+ else:
+ return LogAveragePower(add_db=-70, fft_size=self.fftSize, avg_number=self.fftAverages)
+
+
+class FftChain(Chain):
+ def __init__(self, samp_rate, fft_size, fft_v_overlap_factor, fft_fps, fft_compression):
+ self.sampleRate = samp_rate
+ self.vOverlapFactor = fft_v_overlap_factor
+ self.fps = fft_fps
+ self.size = fft_size
+
+ self.blockSize = 0
+
+ self.fft = Fft(size=self.size, every_n_samples=self.blockSize)
+ self.averager = FftAverager(fft_size=self.size, fft_averages=10)
+ self.fftExchangeSides = FftSwap(fft_size=self.size)
+ workers = [
+ self.fft,
+ self.averager,
+ self.fftExchangeSides,
+ ]
+ self.compressFftAdpcm = None
+ if fft_compression == "adpcm":
+ self.compressFftAdpcm = FftAdpcm(fft_size=self.size)
+ workers += [self.compressFftAdpcm]
+
+ self._updateParameters()
+
+ super().__init__(workers)
+
+ def _setBlockSize(self, fft_block_size):
+ if self.blockSize == int(fft_block_size):
+ return
+ self.blockSize = int(fft_block_size)
+ self.fft.setEveryNSamples(self.blockSize)
+
+ def setVOverlapFactor(self, fft_v_overlap_factor):
+ if self.vOverlapFactor == fft_v_overlap_factor:
+ return
+ self.vOverlapFactor = fft_v_overlap_factor
+ self._updateParameters()
+
+ def setFps(self, fft_fps):
+ if self.fps == fft_fps:
+ return
+ self.fps = fft_fps
+ self._updateParameters()
+
+ def setSampleRate(self, samp_rate):
+ if self.sampleRate == samp_rate:
+ return
+ self.sampleRate = samp_rate
+ self._updateParameters()
+
+ def _updateParameters(self):
+ fftAverages = 0
+
+ if self.vOverlapFactor > 0:
+ fftAverages = int(round(1.0 * self.sampleRate / self.size / self.fps / (1.0 - self.vOverlapFactor)))
+ self.averager.setFftAverages(fftAverages)
+
+ if fftAverages == 0:
+ self._setBlockSize(self.sampleRate / self.fps)
+ else:
+ self._setBlockSize(self.sampleRate / self.fps / fftAverages)
+
+ def setCompression(self, compression: str) -> None:
+ if compression == "adpcm" and not self.compressFftAdpcm:
+ self.compressFftAdpcm = FftAdpcm(self.size)
+ # should always be at the end
+ self.append(self.compressFftAdpcm)
+ elif compression == "none" and self.compressFftAdpcm:
+ self.compressFftAdpcm.stop()
+ self.compressFftAdpcm = None
+ # should always be at that position (right?)
+ self.remove(3)
diff --git a/csdr/chain/freedv.py b/csdr/chain/freedv.py
new file mode 100644
index 000000000..97e2061cd
--- /dev/null
+++ b/csdr/chain/freedv.py
@@ -0,0 +1,28 @@
+from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain
+from csdr.module.freedv import FreeDVModule
+from pycsdr.modules import RealPart, Agc, Convert
+from pycsdr.types import Format
+
+
+class FreeDV(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain):
+ def __init__(self):
+ agc = Agc(Format.SHORT)
+ agc.setMaxGain(30)
+ agc.setInitialGain(3)
+ workers = [
+ RealPart(),
+ Agc(Format.FLOAT),
+ Convert(Format.FLOAT, Format.SHORT),
+ FreeDVModule(),
+ agc,
+ ]
+ super().__init__(workers)
+
+ def getFixedIfSampleRate(self) -> int:
+ return 8000
+
+ def getFixedAudioRate(self) -> int:
+ return 8000
+
+ def supportsSquelch(self) -> bool:
+ return False
diff --git a/csdr/chain/m17.py b/csdr/chain/m17.py
new file mode 100644
index 000000000..937103c3e
--- /dev/null
+++ b/csdr/chain/m17.py
@@ -0,0 +1,29 @@
+from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, MetaProvider
+from csdr.module.m17 import M17Module
+from pycsdr.modules import FmDemod, Limit, Convert, Writer, DcBlock
+from pycsdr.types import Format
+
+
+class M17(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, MetaProvider):
+ def __init__(self):
+ self.module = M17Module()
+ workers = [
+ FmDemod(),
+ DcBlock(),
+ Limit(),
+ Convert(Format.FLOAT, Format.SHORT),
+ self.module,
+ ]
+ super().__init__(workers)
+
+ def getFixedIfSampleRate(self) -> int:
+ return 48000
+
+ def getFixedAudioRate(self) -> int:
+ return 8000
+
+ def supportsSquelch(self) -> bool:
+ return False
+
+ def setMetaWriter(self, writer: Writer) -> None:
+ self.module.setMetaWriter(writer)
diff --git a/csdr/chain/redsea.py b/csdr/chain/redsea.py
new file mode 100644
index 000000000..a03da6866
--- /dev/null
+++ b/csdr/chain/redsea.py
@@ -0,0 +1,14 @@
+from csdr.chain import Chain
+from pycsdr.modules import Convert
+from pycsdr.types import Format
+from owrx.rds.redsea import RedseaModule
+from csdr.module import JsonParser
+
+
+class Redsea(Chain):
+ def __init__(self, sampleRate: int, rbds: bool):
+ super().__init__([
+ Convert(Format.FLOAT, Format.SHORT),
+ RedseaModule(sampleRate, rbds),
+ JsonParser("WFM"),
+ ])
diff --git a/csdr/chain/rtl433.py b/csdr/chain/rtl433.py
new file mode 100644
index 000000000..34d4842ae
--- /dev/null
+++ b/csdr/chain/rtl433.py
@@ -0,0 +1,18 @@
+from owrx.ism.rtl433 import Rtl433Module, IsmParser
+from csdr.chain.demodulator import ServiceDemodulator
+
+
+class Rtl433(ServiceDemodulator):
+ def getFixedAudioRate(self) -> int:
+ return 1200000
+
+ def __init__(self):
+ super().__init__(
+ [
+ Rtl433Module(),
+ IsmParser(),
+ ]
+ )
+
+ def supportsSquelch(self) -> bool:
+ return False
diff --git a/csdr/chain/selector.py b/csdr/chain/selector.py
new file mode 100644
index 000000000..74a86361b
--- /dev/null
+++ b/csdr/chain/selector.py
@@ -0,0 +1,187 @@
+from csdr.chain import Chain
+from pycsdr.modules import Shift, FirDecimate, Bandpass, Squelch, FractionalDecimator, Writer
+from pycsdr.types import Format
+from typing import Union
+import math
+
+
+class Decimator(Chain):
+ def __init__(self, inputRate: int, outputRate: int):
+ if outputRate > inputRate:
+ raise ValueError("impossible decimation: cannot upsample {} to {}".format(inputRate, outputRate))
+ self.inputRate = inputRate
+ self.outputRate = outputRate
+
+ decimation, fraction = self._getDecimation(outputRate)
+ transition = 0.15 * (outputRate / float(self.inputRate))
+ # set the cutoff on the fist decimation stage lower so that the resulting output
+ # is already prepared for the second (fractional) decimation stage.
+ # this spares us a second filter.
+ cutoff = 0.5 * decimation / (self.inputRate / outputRate)
+
+ workers = [
+ FirDecimate(decimation, transition, cutoff),
+ ]
+
+ if fraction != 1.0:
+ workers += [FractionalDecimator(Format.COMPLEX_FLOAT, fraction)]
+
+ super().__init__(workers)
+
+ def _getDecimation(self, outputRate: int) -> (int, float):
+ if outputRate > self.inputRate:
+ raise SelectorError(
+ "cannot provide selected output rate {} since it is bigger than input rate {}".format(
+ outputRate,
+ self.inputRate
+ )
+ )
+ d = self.inputRate / outputRate
+ dInt = int(d)
+ dFloat = float(self.inputRate / dInt) / outputRate
+ return dInt, dFloat
+
+ def _reconfigure(self):
+ decimation, fraction = self._getDecimation(self.outputRate)
+ transition = 0.15 * (self.outputRate / float(self.inputRate))
+ cutoff = 0.5 * decimation / (self.inputRate / self.outputRate)
+ self.replace(0, FirDecimate(decimation, transition, cutoff))
+ index = self.indexOf(lambda x: isinstance(x, FractionalDecimator))
+ if fraction != 1.0:
+ decimator = FractionalDecimator(Format.COMPLEX_FLOAT, fraction)
+ if index >= 0:
+ self.replace(index, decimator)
+ else:
+ self.append(decimator)
+ elif index >= 0:
+ self.remove(index)
+
+ def setOutputRate(self, outputRate: int) -> None:
+ if outputRate == self.outputRate:
+ return
+ self.outputRate = outputRate
+ self._reconfigure()
+
+ def setInputRate(self, inputRate: int) -> None:
+ if inputRate == self.inputRate:
+ return
+ self.inputRate = inputRate
+ self._reconfigure()
+
+
+class Selector(Chain):
+ def __init__(self, inputRate: int, outputRate: int, withSquelch: bool = True):
+ self.inputRate = inputRate
+ self.outputRate = outputRate
+ self.frequencyOffset = 0
+
+ self.shift = Shift(0.0)
+
+ self.decimation = Decimator(inputRate, outputRate)
+
+ self.bandpass = self._buildBandpass()
+ self.bandpassCutoffs = [None, None]
+
+ workers = [self.shift, self.decimation]
+
+ if withSquelch:
+ self.readings_per_second = 4
+ # s-meter readings are available every 1024 samples
+ # the reporting interval is measured in those 1024-sample blocks
+ self.squelch = Squelch(5, int(outputRate / (self.readings_per_second * 1024)))
+ workers += [self.squelch]
+
+ super().__init__(workers)
+
+ def _buildBandpass(self) -> Bandpass:
+ bp_transition = 320.0 / self.outputRate
+ return Bandpass(transition=bp_transition, use_fft=True)
+
+ def setFrequencyOffset(self, offset: int) -> None:
+ if offset == self.frequencyOffset:
+ return
+ self.frequencyOffset = offset
+ self._updateShift()
+
+ def _updateShift(self):
+ shift = -self.frequencyOffset / self.inputRate
+ self.shift.setRate(shift)
+
+ def _convertToLinear(self, db: float) -> float:
+ return float(math.pow(10, db / 10))
+
+ def setSquelchLevel(self, level: float) -> None:
+ self.squelch.setSquelchLevel(self._convertToLinear(level))
+
+ def _enableBandpass(self):
+ index = self.indexOf(lambda x: isinstance(x, Bandpass))
+ if index < 0:
+ self.insert(2, self.bandpass)
+
+ def _disableBandpass(self):
+ index = self.indexOf(lambda x: isinstance(x, Bandpass))
+ if index >= 0:
+ self.remove(index)
+
+ def setBandpass(self, lowCut: float, highCut: float) -> None:
+ self.bandpassCutoffs = [lowCut, highCut]
+ if None in self.bandpassCutoffs:
+ self._disableBandpass()
+ else:
+ self._enableBandpass()
+ scaled = [x / self.outputRate for x in self.bandpassCutoffs]
+ self.bandpass.setBandpass(*scaled)
+
+ def setLowCut(self, lowCut: Union[float, None]) -> None:
+ self.bandpassCutoffs[0] = lowCut
+ self.setBandpass(*self.bandpassCutoffs)
+
+ def setHighCut(self, highCut: Union[float, None]) -> None:
+ self.bandpassCutoffs[1] = highCut
+ self.setBandpass(*self.bandpassCutoffs)
+
+ def setPowerWriter(self, writer: Writer) -> None:
+ self.squelch.setPowerWriter(writer)
+
+ def setOutputRate(self, outputRate: int) -> None:
+ if outputRate == self.outputRate:
+ return
+ self.outputRate = outputRate
+
+ self.decimation.setOutputRate(outputRate)
+ self.squelch.setReportInterval(int(outputRate / (self.readings_per_second * 1024)))
+ index = self.indexOf(lambda x: isinstance(x, Bandpass))
+ self.bandpass = self._buildBandpass()
+ self.setBandpass(*self.bandpassCutoffs)
+ if index >= 0:
+ self.replace(index, self.bandpass)
+
+ def setInputRate(self, inputRate: int) -> None:
+ if inputRate == self.inputRate:
+ return
+ self.inputRate = inputRate
+ self.decimation.setInputRate(inputRate)
+ self._updateShift()
+
+
+class SecondarySelector(Chain):
+ def __init__(self, sampleRate: int, bandwidth: float):
+ self.sampleRate = sampleRate
+ self.frequencyOffset = 0
+ self.shift = Shift(0.0)
+ cutoffRate = bandwidth / sampleRate
+ self.bandpass = Bandpass(-cutoffRate, cutoffRate, cutoffRate, use_fft=True)
+ workers = [self.shift, self.bandpass]
+ super().__init__(workers)
+
+ def setFrequencyOffset(self, offset: int) -> None:
+ if offset == self.frequencyOffset:
+ return
+ self.frequencyOffset = offset
+ if self.frequencyOffset is None:
+ return
+ self.shift.setRate(-offset / self.sampleRate)
+
+
+class SelectorError(Exception):
+ pass
diff --git a/csdr/module/__init__.py b/csdr/module/__init__.py
new file mode 100644
index 000000000..c1fdd88b2
--- /dev/null
+++ b/csdr/module/__init__.py
@@ -0,0 +1,231 @@
+from pycsdr.modules import Module as BaseModule
+from pycsdr.modules import Reader, Writer, Buffer
+from pycsdr.types import Format
+from abc import ABCMeta, abstractmethod
+from threading import Thread
+from io import BytesIO
+from subprocess import Popen, PIPE, TimeoutExpired
+from functools import partial
+import pickle
+import logging
+import json
+
+logger = logging.getLogger(__name__)
+
+
+class Module(BaseModule, metaclass=ABCMeta):
+ def __init__(self):
+ self.reader = None
+ self.writer = None
+ super().__init__()
+
+ def setReader(self, reader: Reader) -> None:
+ self.reader = reader
+
+ def setWriter(self, writer: Writer) -> None:
+ self.writer = writer
+
+ @abstractmethod
+ def getInputFormat(self) -> Format:
+ pass
+
+ @abstractmethod
+ def getOutputFormat(self) -> Format:
+ pass
+
+ def pump(self, read, write):
+ def copy():
+ while True:
+ data = None
+ try:
+ data = read()
+ except ValueError:
+ pass
+ except BrokenPipeError:
+ break
+ if data is None or isinstance(data, bytes) and len(data) == 0:
+ break
+ try:
+ write(data)
+ except BrokenPipeError:
+ break
+
+ return copy
+
+
+class AutoStartModule(Module, metaclass=ABCMeta):
+ def _checkStart(self) -> None:
+ if self.reader is not None and self.writer is not None:
+ self.start()
+
+ def setReader(self, reader: Reader) -> None:
+ super().setReader(reader)
+ self._checkStart()
+
+ def setWriter(self, writer: Writer) -> None:
+ super().setWriter(writer)
+ self._checkStart()
+
+ @abstractmethod
+ def start(self):
+ pass
+
+
+class ThreadModule(AutoStartModule, Thread, metaclass=ABCMeta):
+ def __init__(self):
+ self.doRun = True
+ super().__init__()
+ Thread.__init__(self)
+
+ @abstractmethod
+ def run(self):
+ pass
+
+ def stop(self):
+ self.doRun = False
+ self.reader.stop()
+
+ def start(self):
+ # don't start twice.
+ if self.is_alive():
+ return
+ Thread.start(self)
+
+
+class PickleModule(ThreadModule):
+ def getInputFormat(self) -> Format:
+ return Format.CHAR
+
+ def getOutputFormat(self) -> Format:
+ return Format.CHAR
+
+ def run(self):
+ while self.doRun:
+ data = self.reader.read()
+ if data is None:
+ self.doRun = False
+ break
+ io = BytesIO(data.tobytes())
+ try:
+ while True:
+ output = self.process(pickle.load(io))
+ if output is not None:
+ self.writer.write(pickle.dumps(output))
+ except EOFError:
+ pass
+
+ @abstractmethod
+ def process(self, input):
+ pass
+
+
+class LineBasedModule(ThreadModule, metaclass=ABCMeta):
+ def __init__(self):
+ self.retained = bytes()
+ super().__init__()
+
+ def getInputFormat(self) -> Format:
+ return Format.CHAR
+
+ def getOutputFormat(self) -> Format:
+ return Format.CHAR
+
+ def run(self):
+ while self.doRun:
+ data = self.reader.read()
+ if data is None:
+ self.doRun = False
+ else:
+ self.retained += data
+ lines = self.retained.split(b"\n")
+
+ # keep the last line
+ # this should either be empty if the last char was \n
+ # or an incomplete line if the read returned early
+ self.retained = lines[-1]
+
+ # log all completed lines
+ for line in lines[0:-1]:
+ parsed = self.process(line)
+ if parsed is not None:
+ self.writer.write(pickle.dumps(parsed))
+
+ @abstractmethod
+ def process(self, line: bytes) -> any:
+ pass
+
+
+class JsonParser(LineBasedModule):
+ def __init__(self, mode: str):
+ self.mode = mode
+ super().__init__()
+
+ def process(self, line):
+ try:
+ msg = json.loads(line)
+ msg["mode"] = self.mode
+ logger.debug(msg)
+ return msg
+ except json.JSONDecodeError:
+ logger.exception("error parsing decoder json")
+
+
+class PopenModule(AutoStartModule, metaclass=ABCMeta):
+ def __init__(self):
+ self.process = None
+ super().__init__()
+
+ @abstractmethod
+ def getCommand(self):
+ pass
+
+ def _getProcess(self):
+ return Popen(self.getCommand(), stdin=PIPE, stdout=PIPE)
+
+ def start(self):
+ self.process = self._getProcess()
+ # resume in case the reader has been stop()ed before
+ self.reader.resume()
+ Thread(target=self.pump(self.reader.read, self.process.stdin.write)).start()
+ Thread(target=self.pump(partial(self.process.stdout.read1, 1024), self.writer.write)).start()
+
+ def stop(self):
+ if self.process is not None:
+ # Try terminating normally, kill if failed to terminate
+ try:
+ self.process.terminate()
+ self.process.wait(3)
+ except TimeoutExpired:
+ self.process.kill()
+ self.process = None
+ self.reader.stop()
+
+
+class LogReader(Thread):
+ def __init__(self, prefix: str, buffer: Buffer):
+ self.reader = buffer.getReader()
+ self.logger = logging.getLogger(prefix)
+ self.retained = bytes()
+ super().__init__()
+ self.start()
+
+ def run(self) -> None:
+ while True:
+ data = self.reader.read()
+ if data is None:
+ return
+
+ self.retained += data
+ lines = self.retained.split(b"\n")
+
+ # keep the last line
+ # this should either be empty if the last char was \n
+ # or an incomplete line if the read returned early
+ self.retained = lines[-1]
+
+ # log all completed lines
+ for line in lines[0:-1]:
+ self.logger.info("{}: {}".format("STDOUT", line.decode(errors="replace")))
+
+ def stop(self):
+ self.reader.stop()
diff --git a/csdr/module/drm.py b/csdr/module/drm.py
new file mode 100644
index 000000000..a7515bd8d
--- /dev/null
+++ b/csdr/module/drm.py
@@ -0,0 +1,11 @@
+from pycsdr.modules import ExecModule
+from pycsdr.types import Format
+
+
+class DrmModule(ExecModule):
+ def __init__(self):
+ super().__init__(
+ Format.COMPLEX_SHORT,
+ Format.SHORT,
+ ["dream", "-c", "6", "--sigsrate", "48000", "--audsrate", "48000", "-I", "-", "-O", "-"]
+ )
diff --git a/csdr/module/freedv.py b/csdr/module/freedv.py
new file mode 100644
index 000000000..90bc38a70
--- /dev/null
+++ b/csdr/module/freedv.py
@@ -0,0 +1,11 @@
+from pycsdr.types import Format
+from pycsdr.modules import ExecModule
+
+
+class FreeDVModule(ExecModule):
+ def __init__(self):
+ super().__init__(
+ Format.SHORT,
+ Format.SHORT,
+ ["freedv_rx", "1600", "-", "-"]
+ )
diff --git a/csdr/module/m17.py b/csdr/module/m17.py
new file mode 100644
index 000000000..5bcf5449f
--- /dev/null
+++ b/csdr/module/m17.py
@@ -0,0 +1,58 @@
+from csdr.module import PopenModule
+from pycsdr.types import Format
+from pycsdr.modules import Writer
+from subprocess import Popen, PIPE
+from threading import Thread
+
+import re
+import pickle
+
+
+class M17Module(PopenModule):
+ lsfRegex = re.compile("SRC: ([a-zA-Z0-9]+), DEST: ([a-zA-Z0-9]+)")
+
+ def __init__(self):
+ super().__init__()
+ self.metawriter = None
+
+ def getInputFormat(self) -> Format:
+ return Format.SHORT
+
+ def getOutputFormat(self) -> Format:
+ return Format.SHORT
+
+ def getCommand(self):
+ return ["m17-demod", "-l"]
+
+ def _getProcess(self):
+ return Popen(self.getCommand(), stdin=PIPE, stdout=PIPE, stderr=PIPE)
+
+ def start(self):
+ super().start()
+ Thread(target=self._readOutput).start()
+
+ def _readOutput(self):
+ while True:
+ line = self.process.stderr.readline()
+ if not line:
+ break
+ self.parseOutput(line.decode())
+
+ def parseOutput(self, line):
+ if self.metawriter is None:
+ return
+ matches = self.lsfRegex.match(line)
+ msg = {"protocol": "M17"}
+ if matches:
+ # fake sync
+ msg["sync"] = "voice"
+ msg["source"] = matches.group(1)
+ msg["destination"] = matches.group(2)
+ elif line.startswith("EOS"):
+ pass
+ else:
+ return
+ self.metawriter.write(pickle.dumps(msg))
+
+ def setMetaWriter(self, writer: Writer) -> None:
+ self.metawriter = writer
diff --git a/csdr/module/msk144.py b/csdr/module/msk144.py
new file mode 100644
index 000000000..532ed67fa
--- /dev/null
+++ b/csdr/module/msk144.py
@@ -0,0 +1,33 @@
+from pycsdr.types import Format
+from pycsdr.modules import ExecModule
+from csdr.module import LineBasedModule
+from owrx.wsjt import WsjtParser, Msk144Profile
+import pickle
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+class Msk144Module(ExecModule):
+ def __init__(self):
+ super().__init__(
+ Format.SHORT,
+ Format.CHAR,
+ ["msk144decoder"]
+ )
+
+
+class ParserAdapter(LineBasedModule):
+ def __init__(self):
+ self.parser = WsjtParser()
+ self.dialFrequency = 0
+ self.profile = Msk144Profile()
+ super().__init__()
+
+ def process(self, line: bytes):
+ # actual messages from msk144decoder should start with "*** "
+ if line[0:4] == b"*** ":
+ return self.parser.parse(self.profile, self.dialFrequency, line[4:])
+
+ def setDialFrequency(self, frequency: int) -> None:
+ self.dialFrequency = frequency
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 000000000..aa95d2f47
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,256 @@
+openwebrx (1.3.0) UNRELEASED; urgency=low
+ * SDR device log messages are now available in the web configuration to
+ simplify troubleshooting
+ * Added support for the MSK144 digimode
+ * Added support for decoding ADS-B with dump1090
+ * Added support for decoding HFDL and VDL2 aircraft communications
+ * Added decoding of ISM band transmissions using rtl_433
+ * Added support for decoding RDS data on WFM broadcasts using redsea decoder
+ * Added decoding for DAB broadcast stations using csdr-eti and dablin
+ * Added IPv6 support
+ * Added MQTT support
+ * New devices supported:
+ - Afedri SDR-Net
+
+ -- Jakob Ketterl Fri, 30 Sep 2022 16:47:00 +0000
+
+openwebrx (1.2.2) bullseye jammy; urgency=high
+
+ * - Fixed an over-the-air code injection vulnerability
+
+ -- Jakob Ketterl Sun, 08 Oct 2023 21:29:00 +0000
+
+openwebrx (1.2.1) bullseye jammy; urgency=low
+
+ * FifiSDR support fixed (pipeline formats now line up correctly)
+ * Added "Device" input for FifiSDR devices for sound card selection
+
+ -- Jakob Ketterl Tue, 20 Sep 2022 16:01:00 +0000
+
+openwebrx (1.2.0) bullseye jammy; urgency=low
+
+ * Major rewrite of all demodulation components to make use of the new
+ csdr/pycsdr and digiham/pydigiham demodulator modules
+ * Preliminary display of M17 callsign information
+ * New devices supported:
+ - Blade RF
+
+ -- Jakob Ketterl Wed, 15 Jun 2022 16:20:00 +0000
+
+openwebrx (1.1.0) buster hirsute; urgency=low
+
+ * Reworked most graphical elements as SVGs for faster loadtimes and crispier
+ display on hi-dpi displays
+ * Updated pipelines to match changes in digiham
+ * Changed D-Star and NXDN integrations to use new decoder from digiham
+ * Added D-Star and NXDN metadata display
+
+ -- Jakob Ketterl Mon, 02 Aug 2021 16:24:00 +0000
+
+openwebrx (1.0.0) buster hirsute; urgency=low
+ * Introduced `squelch_auto_margin` config option that allows configuring the
+ auto squelch level
+ * Removed `port` configuration option; `rtltcp_compat` takes the port number
+ with the new connectors
+ * Added support for new WSJT-X modes FST4, FST4W (only available with WSJT-X
+ 2.3) and Q65 (only available with WSJT-X 2.4)
+ * Added support for demodulating M17 digital voice signals using
+ m17-cxx-demod
+ * New reporting infrastructure, allowing WSPR and FST4W spots to be sent to
+ wsprnet.org
+ * Add some basic filtering capabilities to the map
+ * New arguments to the `openwebrx` command-line to facilitate the
+ administration of users (try `openwebrx admin`)
+ * New command-line tool `openwebrx-admin` that facilitates the
+ administration of users
+ * Default bandwidth changes:
+ - "WFM" changed to 150kHz
+ - "Packet" (APRS) changed to 12.5kHz
+ * Configuration rework:
+ - New: fully web-based configuration interface
+ - System configuration parameters have been moved to a new, separate
+ `openwebrx.conf` file
+ - Remaining parameters are now editable in the web configuration
+ - Existing `config_webrx.py` files will still be read, but changes made in
+ the web configuration will be written to a new storage system
+ - Added upload of avatar and panorama image via web configuration
+ * New devices supported:
+ - HPSDR devices (Hermes Lite 2) thanks to @jancona
+ - BBRF103 / RX666 / RX888 devices supported by libsddc
+ - R&S devices using the EB200 or Ammos protocols
+
+ -- Jakob Ketterl Thu, 06 May 2021 17:22:00 +0000
+
+openwebrx (0.20.3) buster focal; urgency=low
+
+ * Fix a compatibility issue with python versions <= 3.6
+
+ -- Jakob Ketterl Tue, 26 Jan 2021 15:28:00 +0000
+
+openwebrx (0.20.2) buster focal; urgency=high
+
+ * Fix a security problem that allowed arbitrary commands to be executed on
+ the receiver (See github issue #215:
+ https://github.com/jketterl/openwebrx/issues/215)
+
+ -- Jakob Ketterl Sun, 24 Jan 2021 22:50:00 +0000
+
+openwebrx (0.20.1) buster focal; urgency=low
+
+ * Remove broken OSM map fallback
+
+ -- Jakob Ketterl Mon, 30 Nov 2020 17:29:00 +0000
+
+openwebrx (0.20.0) buster focal; urgency=low
+
+ * Added the ability to sign multiple keys in a single request, thus enabling
+ multiple users to claim a single receiver on receiverbook.de
+ * Fixed file descriptor leaks to prevent "too many open files" errors
+ * Add new demodulator chain for FreeDV
+ * Added new HD audio streaming mode along with a new WFM demodulator
+ * Reworked AGC code for better results in AM, SSB and digital modes
+ * Added support for demodulation of "Digital Radio Mondiale" (DRM) broadcast
+ using the "dream" decoder.
+ * New default waterfall color scheme
+ * Prototype of a continuous automatic waterfall calibration mode
+ * New devices supported:
+ - FunCube Dongle Pro+ (`"type": "fcdpp"`)
+ - Support for connections to rtl_tcp (`"type": "rtl_tcp"`)
+
+ -- Jakob Ketterl Sun, 11 Oct 2020 13:02:00 +0000
+
+openwebrx (0.19.1) buster focal; urgency=low
+
+ * Added ability to authenticate receivers with listing sites using
+ "receiver id" tokens
+
+ -- Jakob Ketterl Sat, 13 Jun 2020 16:46:00 +0000
+
+openwebrx (0.19.0) buster focal; urgency=low
+ * Fix direwolf connection setup by implementing a retry loop
+ * Pass direct sampling mode changes for rtl_sdr_soapy to owrx_connector
+ * OSM maps instead of Google when google_maps_api_key is not set (thanks
+ @jquagga)
+ * Improved logic to pass parameters to soapy devices.
+ - `rtl_sdr_soapy`: added support for `bias_tee`
+ - `sdrplay`: added support for `bias_tee`, `rf_notch` and `dab_notch`
+ - `airspy`: added support for `bitpack`
+ * Added support for Perseus-SDR devices, (thanks @amontefusco)
+ * Property System has been rewritten so that defaults on sdr behave as
+ expected
+ * Waterfall range auto-adjustment now only takes the center 80% of the
+ spectrum into account, which should work better with SDRs that oversample
+ or have rather flat filter curves towards the spectrum edges
+ * Bugfix for negative network usage
+ * FiFi SDR: prevent arecord from shutting down after 2GB of data has been
+ sent
+ * Added support for bias tee control on rtl_sdr devices
+ * All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC
+ * `rtl_sdr` type now also supports the `direct_sampling` option
+ * Added decoding implementation for for digimode "JS8Call" (requires an
+ installation of js8call and the js8py library)
+ * Reorganization of the frontend demodulator code
+ * Improve receiver load time by concatenating javascript assets
+ * HackRF support is now based on SoapyHackRF
+ * Removed sdr.hu server listing support since the site has been shut down
+ * Added support for Radioberry 2 Rasbperry Pi SDR Cape
+
+ -- Jakob Ketterl Mon, 01 Jun 2020 17:02:00 +0000
+
+openwebrx (0.18.0) buster; urgency=low
+
+ * Compression, resampling and filtering in the frontend have been rewritten
+ in javascript, sdr.js has been removed
+ * Decoding of Pocsag modulation is now possible
+ * Removed the 3D waterfall since it had no real application and required ~1MB
+ of javascript code to be downloaded
+ * Improved the frontend handling of the "too many users" scenario
+ * PSK63 digimode is now available (same decoding pipeline as PSK31, but with
+ adopted parameters)
+ * The frequency can now be manipulated with the mousewheel, which should
+ allow the user to tune more precise. The tuning step size is determined by
+ the digit the mouse cursor is hovering over.
+ * Clicking on the frequency now opens an input for direct frequency selection
+ * URL hashes have been fixed and improved: They are now updated
+ automatically, so a shared URL will include frequency and demodulator,
+ which allows for improved sharing and linking.
+ * New daylight scheduler for background decoding, allows profiles to be
+ selected by local sunrise / sunset times
+ * The owrx_connector is now the default way of communicating with sdr
+ devices. The old sdr types have been replaced, all `_connector` suffixes on
+ the type must be removed!
+ * The sources have been refactored, making it a lot easier to add support for
+ other devices
+ * SDR device failure handling has been improved, including user feedback
+ * New devices supported:
+ * wsjt-x updated to 2.1.2
+ * The rtl_tcp compatibility mode of the owrx_connector is now configurable
+ using the `rtltcp_compat` flag
+ * explicit device filter for soapy devices for multi-device setups
+ * compatibility fixes for safari browsers (ios and mac)
+ * Offset tuning using the `lfo_offset` has been reworked in a way that
+ `center_freq` has to be set to the frequency you actually want to listen
+ to. If you're using an `lfo_offset` already, you will probably need to
+ change its sign.
+ * `initial_squelch_level` can now be set on each profile.
+ * Part of the frontend code has been reworked
+ - Audio buffer minimums have been completely stripped. As a result, you
+ should get better latency. Unfortunately, this also means there will be
+ some skipping when audio starts.
+ - Now also supports AudioWorklets (for those browser that have it).
+ - Mousewheel controls for the receiver sliders
+ * Error handling for failed SDR devices
+ * One of the most-requested features is finally coming to OpenWebRX:
+ Bookmarks (sometimes also referred to as labels).
+ There's two kinds of bookmarks available:
+ - Serverside bookmarks that are set up by the receiver administrator.
+ Check the file `bookmarks.json` for examples!
+ - Clientside bookmarks which every user can store for themselves. They are
+ stored in the browser's localStorage.
+ * Automatic reporting of spots to [pskreporter](https://pskreporter.info/) is
+ now possible. Please have a look at the configuration on how to set it up.
+ * Websocket communication has been overhauled in large parts. It should now
+ be more reliable, and failing connections should now have no impact on
+ other users.
+ * Profile scheduling allows to set up band-hopping if you are running
+ background services.
+ * APRS now has the ability to show symbols on the map, if a corresponding
+ symbol set has been installed. Check the config!
+ * Debug logging has been disabled in a handful of modules, expect vastly
+ reduced output on the shell.
+ * New set of APRS-related features
+ - Decode Packet transmissions using direwolf (1k2 only for now)
+ - APRS packets are mostly decoded and shown both in a new panel and on the
+ map
+ - APRS is also available as a background service
+ - direwolfs I-gate functionality can be enabled, which allows your receiver
+ to work as a receive-only I-gate for the APRS network in the background
+ * Demodulation for background services has been optimized to use less total
+ bandwidth, saving CPU
+ * More metrics have been added; they can be used together with collectd and
+ its curl_json plugin for now, with some limitations.
+ * New bandplan feature, the first thing visible is the "dial" indicator that
+ brings you right to the dial frequency for digital modes
+ * fixed some bugs in the websocket communication which broke the map
+ * WSJT-X integration (FT8, FT4, WSPR, JT65, JT9 using wsjt-x demodulators)
+ * New Map Feature that shows both decoded grid squares from FT8 and Locations
+ decoded from YSF digital voice
+ * New Feature report that will show what functionality is available
+ * major rework on the openwebrx core
+ * Support of multiple SDR devices simultaneously
+ * Support for multiple profiles per SDR that allow the user to listen to
+ different frequencies
+ * Support for digital voice decoding
+ * Feature detection that will disable functionality when dependencies are not
+ available (if you're missing the digital
+ buttons, this is probably why)
+ * Support added for the following SDR sources:
+ - LimeSDR (`"type": "lime_sdr"`)
+ - PlutoSDR (`"type": "pluto_sdr"`)
+ - RTL_SDR via Soapy (`"type": "rtl_sdr_soapy"`) on special request to allow
+ use of the direct sampling mode
+ - SoapyRemote (`"type": "soapy_remote"`)
+ - FiFiSDR (`"type": "fifi_sdr"`)
+ - airspyhf devices (Airspy HF+ / Discovery) (`"type": "airspyhf"`)
+
+ -- Jakob Ketterl Tue, 18 Feb 2020 20:09:00 +0000
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 000000000..f599e28b8
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+10
diff --git a/debian/control b/debian/control
new file mode 100644
index 000000000..ae30de550
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,50 @@
+Source: openwebrx
+Maintainer: Jakob Ketterl
+Section: hamradio
+Priority: optional
+Rules-Requires-Root: no
+Standards-Version: 4.2.0
+Build-Depends: debhelper (>= 11),
+ dh-python,
+ python3-all (>= 3.5),
+ python3-setuptools
+Homepage: https://www.openwebrx.de/
+Vcs-Browser: https://github.com/jketterl/openwebrx
+Vcs-Git: https://github.com/jketterl/openwebrx.git
+
+Package: openwebrx
+Architecture: all
+Depends: adduser,
+ python3 (>= 3.5),
+ python3-setuptools,
+ owrx-connector (>= 0.7),
+ python3-csdr (>= 0.19),
+ ${python3:Depends},
+ ${misc:Depends}
+Recommends: python3-digiham (>= 0.6),
+ direwolf (>= 1.4),
+ wsjtx,
+ js8call,
+ runds-connector (>= 0.2),
+ hpsdrconnector,
+ aprs-symbols,
+ m17-demod,
+ js8call,
+ python3-js8py (>= 0.2),
+ nmux (>= 0.18),
+ codecserver (>= 0.1),
+ msk144decoder,
+ dump1090-fa-minimal,
+ dumphfdl,
+ dumpvdl2,
+ rtl-433,
+ extra-sdr-drivers,
+ perseus-tools,
+ dream-headless,
+ codec2,
+ redsea,
+ python3-csdr-eti,
+ dablin,
+ python3-paho-mqtt
+Description: multi-user web sdr
+ Open source, multi-user SDR receiver with a web interface
diff --git a/debian/openwebrx.config b/debian/openwebrx.config
new file mode 100755
index 000000000..2f17fb9a4
--- /dev/null
+++ b/debian/openwebrx.config
@@ -0,0 +1,9 @@
+#!/bin/sh -e
+. /usr/share/debconf/confmodule
+
+db_get openwebrx/admin_user_configured
+if [ "${1:-}" = "reconfigure" ] || [ "${RET}" != true ]; then
+ db_settitle openwebrx/title
+ db_input high openwebrx/admin_user_password || true
+ db_go
+fi
diff --git a/debian/openwebrx.desktop b/debian/openwebrx.desktop
new file mode 100644
index 000000000..e28674cf2
--- /dev/null
+++ b/debian/openwebrx.desktop
@@ -0,0 +1,8 @@
+[Desktop Entry]
+Version=1.0
+Name=OpenWebRX
+Type=Application
+Comment=Web-based software defined radio receiver
+Icon=openwebrx
+Exec=xdg-open http://localhost:8073/
+Categories=Network;HamRadio
diff --git a/debian/openwebrx.dirs b/debian/openwebrx.dirs
new file mode 100644
index 000000000..c87b1b22d
--- /dev/null
+++ b/debian/openwebrx.dirs
@@ -0,0 +1 @@
+/etc/openwebrx/openwebrx.conf.d
\ No newline at end of file
diff --git a/debian/openwebrx.install b/debian/openwebrx.install
new file mode 100644
index 000000000..9b89f9f26
--- /dev/null
+++ b/debian/openwebrx.install
@@ -0,0 +1,5 @@
+bands.json etc/openwebrx/
+openwebrx.conf etc/openwebrx/
+systemd/openwebrx.service lib/systemd/system/
+debian/openwebrx.svg usr/share/icons/hicolor/scalable/apps
+debian/openwebrx.desktop usr/share/applications
\ No newline at end of file
diff --git a/debian/openwebrx.postinst b/debian/openwebrx.postinst
new file mode 100755
index 000000000..a7e0a5138
--- /dev/null
+++ b/debian/openwebrx.postinst
@@ -0,0 +1,66 @@
+#!/bin/bash
+. /usr/share/debconf/confmodule
+
+set -euo pipefail
+
+OWRX_USER="openwebrx"
+OWRX_DATADIR="/var/lib/openwebrx"
+OWRX_USERS_FILE="${OWRX_DATADIR}/users.json"
+OWRX_SETTINGS_FILE="${OWRX_DATADIR}/settings.json"
+OWRX_BOOKMARKS_FILE="${OWRX_DATADIR}/bookmarks.json"
+
+case "$1" in
+ configure|reconfigure)
+ adduser --system --group --no-create-home --home /nonexistent --quiet "${OWRX_USER}"
+ usermod -aG plugdev "${OWRX_USER}"
+
+ # ensure group exists first (dependency is optional)
+ # addgroup will error out if the group exists, but is not a system group. it doesn't matter for the intended purpose, but we need extra protection for this case.
+ if [ ! $(getent group perseususb) ]; then
+ addgroup --system --quiet perseususb
+ fi
+ usermod -aG perseususb "${OWRX_USER}"
+
+ # create OpenWebRX data directory and set the correct permissions
+ if [ ! -d "${OWRX_DATADIR}" ] && [ ! -L "${OWRX_DATADIR}" ]; then mkdir "${OWRX_DATADIR}"; fi
+ chown "${OWRX_USER}": ${OWRX_DATADIR}
+
+ # create empty config files now to avoid permission problems later
+ if [ ! -e "${OWRX_USERS_FILE}" ]; then
+ echo "[]" > "${OWRX_USERS_FILE}"
+ chown "${OWRX_USER}": "${OWRX_USERS_FILE}"
+ chmod 0600 "${OWRX_USERS_FILE}"
+ fi
+
+ if [ ! -e "${OWRX_SETTINGS_FILE}" ]; then
+ echo "{}" > "${OWRX_SETTINGS_FILE}"
+ chown "${OWRX_USER}": "${OWRX_SETTINGS_FILE}"
+ fi
+
+ if [ ! -e "${OWRX_BOOKMARKS_FILE}" ]; then
+ touch "${OWRX_BOOKMARKS_FILE}"
+ chown "${OWRX_USER}": "${OWRX_BOOKMARKS_FILE}"
+ fi
+
+ db_get openwebrx/admin_user_password
+ if [ ! -z "${RET}" ]; then
+ if ! openwebrx admin --silent hasuser admin; then
+ # create initial openwebrx user
+ OWRX_PASSWORD="${RET}" openwebrx admin --noninteractive adduser admin
+ else
+ # change existing user's password
+ OWRX_PASSWORD="${RET}" openwebrx admin --noninteractive resetpassword admin
+ fi
+ fi
+ # remove password from debconf database
+ db_unregister openwebrx/admin_user_password
+ # set a marker that admin is configured to avoid future questions
+ db_set openwebrx/admin_user_configured true
+ ;;
+ *)
+ echo "postinst called with unknown argument '$1'" 1>&2
+ exit 1
+ ;;
+esac
+
+#DEBHELPER#
diff --git a/debian/openwebrx.postrm b/debian/openwebrx.postrm
new file mode 100755
index 000000000..9260b8ec4
--- /dev/null
+++ b/debian/openwebrx.postrm
@@ -0,0 +1,8 @@
+#!/bin/sh -e
+
+if [ "$1" = purge ] && [ -e /usr/share/debconf/confmodule ]; then
+ . /usr/share/debconf/confmodule
+ db_purge
+fi
+
+#DEBHELPER#
diff --git a/debian/openwebrx.svg b/debian/openwebrx.svg
new file mode 100644
index 000000000..50f4acdde
--- /dev/null
+++ b/debian/openwebrx.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/debian/openwebrx.templates b/debian/openwebrx.templates
new file mode 100644
index 000000000..6819fbadc
--- /dev/null
+++ b/debian/openwebrx.templates
@@ -0,0 +1,27 @@
+Template: openwebrx/admin_user_password
+Type: password
+Description: OpenWebRX "admin" user password:
+ The system can create a user for the OpenWebRX web configuration interface for
+ you. Using this user, you will be able to log into the "settings" area of
+ OpenWebRX to configure your receiver conveniently through your browser.
+ .
+ The name of the created user will be "admin".
+ .
+ If you do not wish to create a web admin user right now, you can leave this
+ empty for now. You can return to this prompt at a later time by running the
+ command "sudo dpkg-reconfigure openwebrx".
+ .
+ You can also use the "openwebrx admin" command to create, delete or manage
+ existing users. More information is available in by running the command
+ "openwebrx admin --help".
+
+Template: openwebrx/admin_user_configured
+Type: boolean
+Default: false
+Description: OpenWebRX "admin" user previously configured?
+ Marker used internally by the config scripts to remember if an admin user has
+ been created.
+
+Template: openwebrx/title
+Type: title
+Description: Configuring OpenWebRX
\ No newline at end of file
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 000000000..3b7418e3e
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,8 @@
+#!/usr/bin/make -f
+export PYBUILD_NAME=openwebrx
+
+%:
+ dh $@ --with python3 --buildsystem=pybuild --with systemd
+
+override_dh_strip_nondeterminism:
+ dh_strip_nondeterminism -X.png
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 000000000..9f6742789
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
\ No newline at end of file
diff --git a/docker.sh b/docker.sh
new file mode 100755
index 000000000..b3657af65
--- /dev/null
+++ b/docker.sh
@@ -0,0 +1,97 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ARCH=$(uname -m)
+IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-afedri openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-fcdpp openwebrx-radioberry openwebrx-uhd openwebrx-rtltcp openwebrx-runds openwebrx-hpsdr openwebrx-bladerf openwebrx-full openwebrx"
+ALL_ARCHS="x86_64 armv7l aarch64"
+TAG=${TAG:-"latest"}
+ARCHTAG="${TAG}-${ARCH}"
+
+usage () {
+ echo "Usage: ${0} [command]"
+ echo "Available commands:"
+ echo " help Show this usage information"
+ echo " build Build all docker images"
+ echo " push Push built docker images to the docker hub"
+ echo " manifest Compile the docker hub manifest (combines arm and x86 tags into one)"
+ echo " tag Tag a release"
+}
+
+build () {
+ # build the base images
+ docker build --pull -t openwebrx-base:${ARCHTAG} -f docker/Dockerfiles/Dockerfile-base .
+ docker build --build-arg ARCHTAG=${ARCHTAG} -t openwebrx-soapysdr-base:${ARCHTAG} -f docker/Dockerfiles/Dockerfile-soapysdr .
+
+ for image in ${IMAGES}; do
+ i=${image:10}
+ # "openwebrx" is a special image that gets tag-aliased later on
+ if [[ ! -z "${i}" ]] ; then
+ docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/${image}:${ARCHTAG} -f docker/Dockerfiles/Dockerfile-${i} .
+ fi
+ done
+
+ # tag openwebrx alias image
+ docker tag jketterl/openwebrx-full:${ARCHTAG} jketterl/openwebrx:${ARCHTAG}
+}
+
+push () {
+ for image in ${IMAGES}; do
+ docker push jketterl/${image}:${ARCHTAG}
+ done
+}
+
+manifest () {
+ for image in ${IMAGES}; do
+ # there's no docker manifest rm command, and the create --amend does not work, so we have to clean up manually
+ rm -rf "${HOME}/.docker/manifests/docker.io_jketterl_${image}-${TAG}"
+ IMAGE_LIST=""
+ for a in ${ALL_ARCHS}; do
+ IMAGE_LIST="${IMAGE_LIST} jketterl/${image}:${TAG}-${a}"
+ done
+ docker manifest create jketterl/${image}:${TAG} ${IMAGE_LIST}
+ docker manifest push --purge jketterl/${image}:${TAG}
+ done
+}
+
+tag () {
+ if [[ -x ${1:-} || -z ${2:-} ]] ; then
+ echo "Usage: ${0} tag [SRC_TAG] [TARGET_TAG]"
+ return
+ fi
+
+ local SRC_TAG=${1}
+ local TARGET_TAG=${2}
+
+ for image in ${IMAGES}; do
+ # there's no docker manifest rm command, and the create --amend does not work, so we have to clean up manually
+ rm -rf "${HOME}/.docker/manifests/docker.io_jketterl_${image}-${TARGET_TAG}"
+ IMAGE_LIST=""
+ for a in ${ALL_ARCHS}; do
+ docker pull jketterl/${image}:${SRC_TAG}-${a}
+ docker tag jketterl/${image}:${SRC_TAG}-${a} jketterl/${image}:${TARGET_TAG}-${a}
+ docker push jketterl/${image}:${TARGET_TAG}-${a}
+ IMAGE_LIST="${IMAGE_LIST} jketterl/${image}:${TARGET_TAG}-${a}"
+ done
+ docker manifest create jketterl/${image}:${TARGET_TAG} ${IMAGE_LIST}
+ docker manifest push --purge jketterl/${image}:${TARGET_TAG}
+ docker pull jketterl/${image}:${TARGET_TAG}
+ done
+}
+
+case ${1:-} in
+ build)
+ build
+ ;;
+ push)
+ push
+ ;;
+ manifest)
+ manifest
+ ;;
+ tag)
+ tag ${@:2}
+ ;;
+ *)
+ usage
+ ;;
+esac
\ No newline at end of file
diff --git a/docker/Dockerfiles/Dockerfile-afedri b/docker/Dockerfiles/Dockerfile-afedri
new file mode 100644
index 000000000..ad7c88e9c
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-afedri
@@ -0,0 +1,8 @@
+ARG ARCHTAG
+FROM openwebrx-soapysdr-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-afedri.sh /
+RUN /install-dependencies-afedri.sh &&\
+ rm /install-dependencies-afedri.sh
+
+ADD . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-airspy b/docker/Dockerfiles/Dockerfile-airspy
new file mode 100644
index 000000000..94b348bd9
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-airspy
@@ -0,0 +1,8 @@
+ARG ARCHTAG
+FROM openwebrx-soapysdr-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-airspy.sh /
+RUN /install-dependencies-airspy.sh &&\
+ rm /install-dependencies-airspy.sh
+
+ADD . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-base b/docker/Dockerfiles/Dockerfile-base
new file mode 100644
index 000000000..e50f03a61
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-base
@@ -0,0 +1,28 @@
+FROM debian:bookworm-slim
+
+COPY docker/files/js8call/js8call-hamlib.patch \
+ docker/files/wsjtx/wsjtx.patch \
+ docker/files/wsjtx/wsjtx-hamlib.patch \
+ docker/files/dream/dream.patch \
+ docker/files/direwolf/direwolf-hamlib.patch \
+ docker/scripts/install-dependencies.sh /
+RUN /install-dependencies.sh && \
+ rm /install-dependencies.sh && \
+ rm /*.patch
+COPY docker/scripts/install-owrx-tools.sh /
+RUN /install-owrx-tools.sh && \
+ rm /install-owrx-tools.sh
+
+COPY docker/files/services/codecserver /etc/services.d/codecserver
+
+ENTRYPOINT ["/init"]
+
+WORKDIR /opt/openwebrx
+
+VOLUME /etc/openwebrx
+VOLUME /var/lib/openwebrx
+
+ENV S6_CMD_ARG0="/opt/openwebrx/docker/scripts/run.sh"
+CMD [""]
+
+EXPOSE 8073
diff --git a/docker/Dockerfiles/Dockerfile-bladerf b/docker/Dockerfiles/Dockerfile-bladerf
new file mode 100644
index 000000000..badcf77ae
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-bladerf
@@ -0,0 +1,8 @@
+ARG ARCHTAG
+FROM openwebrx-soapysdr-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-bladerf.sh /
+RUN /install-dependencies-bladerf.sh &&\
+ rm /install-dependencies-bladerf.sh
+
+COPY . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-fcdpp b/docker/Dockerfiles/Dockerfile-fcdpp
new file mode 100644
index 000000000..3e28ac7fc
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-fcdpp
@@ -0,0 +1,8 @@
+ARG ARCHTAG
+FROM openwebrx-soapysdr-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-fcdpp.sh /
+RUN /install-dependencies-fcdpp.sh &&\
+ rm /install-dependencies-fcdpp.sh
+
+COPY . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-full b/docker/Dockerfiles/Dockerfile-full
new file mode 100644
index 000000000..6d68e8a0d
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-full
@@ -0,0 +1,32 @@
+ARG ARCHTAG
+FROM openwebrx-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-*.sh \
+ docker/files/sdrplay/install-lib.*.patch \
+ docker/scripts/install-connectors.sh /
+
+RUN /install-dependencies-rtlsdr.sh &&\
+ /install-dependencies-soapysdr.sh &&\
+ /install-dependencies-hackrf.sh &&\
+ /install-dependencies-sdrplay.sh &&\
+ /install-dependencies-airspy.sh &&\
+ /install-dependencies-afedri.sh &&\
+ /install-dependencies-rtlsdr-soapy.sh &&\
+ /install-dependencies-plutosdr.sh &&\
+ /install-dependencies-limesdr.sh &&\
+ /install-dependencies-soapyremote.sh &&\
+ /install-dependencies-perseus.sh &&\
+ /install-dependencies-fcdpp.sh &&\
+ /install-dependencies-radioberry.sh &&\
+ /install-dependencies-uhd.sh &&\
+ /install-dependencies-hpsdr.sh &&\
+ /install-dependencies-bladerf.sh &&\
+ /install-connectors.sh &&\
+ /install-dependencies-runds.sh &&\
+ rm /install-dependencies-*.sh &&\
+ rm /install-lib.*.patch && \
+ rm /install-connectors.sh
+
+COPY docker/files/services/sdrplay /etc/services.d/sdrplay
+
+ADD . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-hackrf b/docker/Dockerfiles/Dockerfile-hackrf
new file mode 100644
index 000000000..6dab0f156
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-hackrf
@@ -0,0 +1,8 @@
+ARG ARCHTAG
+FROM openwebrx-soapysdr-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-hackrf.sh /
+RUN /install-dependencies-hackrf.sh &&\
+ rm /install-dependencies-hackrf.sh
+
+COPY . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-hpsdr b/docker/Dockerfiles/Dockerfile-hpsdr
new file mode 100644
index 000000000..96d58b915
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-hpsdr
@@ -0,0 +1,9 @@
+ARG ARCHTAG
+FROM openwebrx-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-hpsdr.sh /
+
+RUN /install-dependencies-hpsdr.sh &&\
+ rm /install-dependencies-hpsdr.sh
+
+COPY . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-limesdr b/docker/Dockerfiles/Dockerfile-limesdr
new file mode 100644
index 000000000..9603c601f
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-limesdr
@@ -0,0 +1,8 @@
+ARG ARCHTAG
+FROM openwebrx-soapysdr-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-limesdr.sh /
+RUN /install-dependencies-limesdr.sh &&\
+ rm /install-dependencies-limesdr.sh
+
+COPY . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-perseus b/docker/Dockerfiles/Dockerfile-perseus
new file mode 100644
index 000000000..bc16583cc
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-perseus
@@ -0,0 +1,8 @@
+ARG ARCHTAG
+FROM openwebrx-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-perseus.sh /
+RUN /install-dependencies-perseus.sh &&\
+ rm /install-dependencies-perseus.sh
+
+COPY . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-plutosdr b/docker/Dockerfiles/Dockerfile-plutosdr
new file mode 100644
index 000000000..4a263e856
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-plutosdr
@@ -0,0 +1,8 @@
+ARG ARCHTAG
+FROM openwebrx-soapysdr-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-plutosdr.sh /
+RUN /install-dependencies-plutosdr.sh &&\
+ rm /install-dependencies-plutosdr.sh
+
+COPY . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-radioberry b/docker/Dockerfiles/Dockerfile-radioberry
new file mode 100644
index 000000000..3cbe978a2
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-radioberry
@@ -0,0 +1,8 @@
+ARG ARCHTAG
+FROM openwebrx-soapysdr-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-radioberry.sh /
+RUN /install-dependencies-radioberry.sh &&\
+ rm /install-dependencies-radioberry.sh
+
+COPY . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-rtlsdr b/docker/Dockerfiles/Dockerfile-rtlsdr
new file mode 100644
index 000000000..614464180
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-rtlsdr
@@ -0,0 +1,12 @@
+ARG ARCHTAG
+FROM openwebrx-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-rtlsdr.sh \
+ docker/scripts/install-connectors.sh /
+
+RUN /install-dependencies-rtlsdr.sh &&\
+ rm /install-dependencies-rtlsdr.sh &&\
+ /install-connectors.sh &&\
+ rm /install-connectors.sh
+
+COPY . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-rtlsdr-soapy b/docker/Dockerfiles/Dockerfile-rtlsdr-soapy
new file mode 100644
index 000000000..5dce90fda
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-rtlsdr-soapy
@@ -0,0 +1,8 @@
+ARG ARCHTAG
+FROM openwebrx-soapysdr-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-rtlsdr-soapy.sh /
+RUN /install-dependencies-rtlsdr-soapy.sh &&\
+ rm /install-dependencies-rtlsdr-soapy.sh
+
+COPY . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-rtltcp b/docker/Dockerfiles/Dockerfile-rtltcp
new file mode 100644
index 000000000..240799dca
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-rtltcp
@@ -0,0 +1,9 @@
+ARG ARCHTAG
+FROM openwebrx-base:$ARCHTAG
+
+COPY docker/scripts/install-connectors.sh /
+
+RUN /install-connectors.sh &&\
+ rm /install-connectors.sh
+
+COPY . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-runds b/docker/Dockerfiles/Dockerfile-runds
new file mode 100644
index 000000000..2a087e1ff
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-runds
@@ -0,0 +1,12 @@
+ARG ARCHTAG
+FROM openwebrx-base:$ARCHTAG
+
+COPY docker/scripts/install-connectors.sh \
+ docker/scripts/install-dependencies-runds.sh /
+
+RUN /install-connectors.sh &&\
+ rm /install-connectors.sh && \
+ /install-dependencies-runds.sh && \
+ rm /install-dependencies-runds.sh
+
+COPY . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-sdrplay b/docker/Dockerfiles/Dockerfile-sdrplay
new file mode 100644
index 000000000..bb53d7ead
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-sdrplay
@@ -0,0 +1,12 @@
+ARG ARCHTAG
+FROM openwebrx-soapysdr-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-sdrplay.sh \
+ docker/files/sdrplay/install-lib.*.patch /
+RUN /install-dependencies-sdrplay.sh &&\
+ rm /install-dependencies-sdrplay.sh &&\
+ rm /install-lib.*.patch
+
+COPY docker/files/services/sdrplay /etc/services.d/sdrplay
+
+COPY . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-soapyremote b/docker/Dockerfiles/Dockerfile-soapyremote
new file mode 100644
index 000000000..e5c207c84
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-soapyremote
@@ -0,0 +1,8 @@
+ARG ARCHTAG
+FROM openwebrx-soapysdr-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-soapyremote.sh /
+RUN /install-dependencies-soapyremote.sh &&\
+ rm /install-dependencies-soapyremote.sh
+
+COPY . /opt/openwebrx
diff --git a/docker/Dockerfiles/Dockerfile-soapysdr b/docker/Dockerfiles/Dockerfile-soapysdr
new file mode 100644
index 000000000..45ac693b6
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-soapysdr
@@ -0,0 +1,9 @@
+ARG ARCHTAG
+FROM openwebrx-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-soapysdr.sh \
+ docker/scripts/install-connectors.sh /
+RUN /install-dependencies-soapysdr.sh &&\
+ rm /install-dependencies-soapysdr.sh &&\
+ /install-connectors.sh &&\
+ rm /install-connectors.sh
diff --git a/docker/Dockerfiles/Dockerfile-uhd b/docker/Dockerfiles/Dockerfile-uhd
new file mode 100644
index 000000000..ae1e758a0
--- /dev/null
+++ b/docker/Dockerfiles/Dockerfile-uhd
@@ -0,0 +1,8 @@
+ARG ARCHTAG
+FROM openwebrx-soapysdr-base:$ARCHTAG
+
+COPY docker/scripts/install-dependencies-uhd.sh /
+RUN /install-dependencies-uhd.sh &&\
+ rm /install-dependencies-uhd.sh
+
+COPY . /opt/openwebrx
diff --git a/docker/files/direwolf/direwolf-hamlib.patch b/docker/files/direwolf/direwolf-hamlib.patch
new file mode 100644
index 000000000..2347c24f9
--- /dev/null
+++ b/docker/files/direwolf/direwolf-hamlib.patch
@@ -0,0 +1,20 @@
+diff --git a/CMakeLists.txt b/CMakeLists.txt
+index 9e710f5..da90b43 100644
+--- a/CMakeLists.txt
++++ b/CMakeLists.txt
+@@ -257,13 +257,8 @@ else()
+ set(GPSD_LIBRARIES "")
+ endif()
+
+-find_package(hamlib)
+-if(HAMLIB_FOUND)
+- set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_HAMLIB")
+-else()
+- set(HAMLIB_INCLUDE_DIRS "")
+- set(HAMLIB_LIBRARIES "")
+-endif()
++set(HAMLIB_INCLUDE_DIRS "")
++set(HAMLIB_LIBRARIES "")
+
+ if(LINUX)
+ find_package(ALSA REQUIRED)
diff --git a/docker/files/dream/dream.patch b/docker/files/dream/dream.patch
new file mode 100644
index 000000000..58de6c4d3
--- /dev/null
+++ b/docker/files/dream/dream.patch
@@ -0,0 +1,96 @@
+--- dream.pro.org 2020-09-04 22:51:51.579926191 +0200
++++ dream.pro 2020-09-04 22:52:57.609434707 +0200
+@@ -70,9 +70,6 @@
+ exists(/opt/local/include/speex/speex_preprocess.h) {
+ CONFIG += speexdsp
+ }
+- exists(/opt/local/include/hamlib/rig.h) {
+- CONFIG += hamlib
+- }
+ contains(QT_VERSION, ^4\\.7.*) {
+ QT += phonon opengl svg
+ DEFINES -= QWT_NO_SVG
+@@ -138,12 +135,6 @@
+ packagesExist(sndfile) {
+ CONFIG += sndfile
+ }
+- packagesExist(hamlib) {
+- CONFIG += hamlib
+- }
+- packagesExist(gpsd) {
+- CONFIG += gps
+- }
+ packagesExist(pcap) {
+ CONFIG += pcap
+ }
+@@ -159,14 +150,6 @@
+ exists(/usr/local/include/sndfile.h) {
+ CONFIG += sndfile
+ }
+- exists(/usr/include/hamlib/rig.h) | \
+- exists(/usr/local/include/hamlib/rig.h) {
+- CONFIG += hamlib
+- }
+- exists(/usr/include/gps.h) | \
+- exists(/usr/local/include/gps.h) {
+- CONFIG += gps
+- }
+ exists(/usr/include/pcap.h) | \
+ exists(/usr/local/include/pcap.h) {
+ CONFIG += pcap
+@@ -194,9 +177,6 @@
+ exists($$OUT_PWD/include/speex/speex_preprocess.h) {
+ CONFIG += speexdsp
+ }
+- exists($$OUT_PWD/include/hamlib/rig.h) {
+- CONFIG += hamlib
+- }
+ exists($$OUT_PWD/include/pcap.h) {
+ CONFIG += pcap
+ }
+@@ -225,7 +205,7 @@
+ LIBS += -lz
+ }
+ }
+-exists($$OUT_PWD/include/neaacdec.h) {
++exists(/usr/include/neaacdec.h) {
+ DEFINES += HAVE_LIBFAAD \
+ USE_FAAD2_LIBRARY
+ LIBS += -lfaad_drm
+@@ -257,11 +237,6 @@
+ win32:LIBS += libspeexdsp.lib
+ message("with libspeexdsp")
+ }
+-gps {
+- DEFINES += HAVE_LIBGPS
+- unix:LIBS += -lgps
+- message("with gps")
+-}
+ pcap {
+ DEFINES += HAVE_LIBPCAP
+ unix:LIBS += -lpcap
+@@ -269,24 +244,6 @@
+ win32-g++:LIBS += -lwpcap -lpacket
+ message("with pcap")
+ }
+-hamlib {
+- DEFINES += HAVE_LIBHAMLIB
+- macx:LIBS += -framework IOKit
+- unix:LIBS += -lhamlib
+- win32:LIBS += libhamlib-2.lib
+- HEADERS += src/util/Hamlib.h
+- SOURCES += src/util/Hamlib.cpp
+- qt {
+- HEADERS += src/util-QT/Rig.h
+- SOURCES += src/util-QT/Rig.cpp
+- }
+- gui {
+- HEADERS += src/GUI-QT/RigDlg.h
+- SOURCES += src/GUI-QT/RigDlg.cpp
+- FORMS += RigDlg.ui
+- }
+- message("with hamlib")
+-}
+ qwt {
+ DEFINES += QWT_NO_SVG
+ macx {
diff --git a/docker/files/js8call/js8call-hamlib.patch b/docker/files/js8call/js8call-hamlib.patch
new file mode 100644
index 000000000..899f83e31
--- /dev/null
+++ b/docker/files/js8call/js8call-hamlib.patch
@@ -0,0 +1,151 @@
+diff -ur js8call-orig/CMake/Modules/Findhamlib.cmake js8call/CMake/Modules/Findhamlib.cmake
+--- js8call-orig/CMake/Modules/Findhamlib.cmake 2020-07-22 18:14:18.014499840 +0200
++++ js8call/CMake/Modules/Findhamlib.cmake 2020-07-22 18:16:07.200375473 +0200
+@@ -78,4 +78,4 @@
+ # Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to
+ # TRUE if all listed variables are TRUE
+ include (FindPackageHandleStandardArgs)
+-find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES hamlib_LIBRARY_DIRS)
++find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES)
+diff -ur js8call-orig/CMakeLists.txt js8call/CMakeLists.txt
+--- js8call-orig/CMakeLists.txt 2020-07-22 18:14:18.014499840 +0200
++++ js8call/CMakeLists.txt 2020-07-22 18:17:55.629633825 +0200
+@@ -558,7 +558,7 @@
+ #
+ # libhamlib setup
+ #
+-set (hamlib_STATIC 1)
++set (hamlib_STATIC 0)
+ find_package (hamlib 3 REQUIRED)
+ find_program (RIGCTL_EXE rigctl)
+ find_program (RIGCTLD_EXE rigctld)
+@@ -911,56 +911,6 @@
+ target_link_libraries (js8 wsjt_fort wsjt_cxx Qt5::Core)
+ endif (${OPENMP_FOUND} OR APPLE)
+
+-# build the main application
+-add_executable (js8call MACOSX_BUNDLE
+- ${sqlite3_CSRCS}
+- ${wsjtx_CXXSRCS}
+- ${wsjtx_GENUISRCS}
+- wsjtx.rc
+- ${WSJTX_ICON_FILE}
+- ${wsjtx_RESOURCES_RCC}
+- images.qrc
+- )
+-
+-if (WSJT_CREATE_WINMAIN)
+- set_target_properties (js8call PROPERTIES WIN32_EXECUTABLE ON)
+-endif (WSJT_CREATE_WINMAIN)
+-
+-set_target_properties (js8call PROPERTIES
+- MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Darwin/Info.plist.in"
+- MACOSX_BUNDLE_INFO_STRING "${WSJTX_DESCRIPTION_SUMMARY}"
+- MACOSX_BUNDLE_ICON_FILE "${WSJTX_ICON_FILE}"
+- MACOSX_BUNDLE_BUNDLE_VERSION ${wsjtx_VERSION}
+- MACOSX_BUNDLE_SHORT_VERSION_STRING "v${wsjtx_VERSION}"
+- MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${wsjtx_VERSION}"
+- MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}"
+- MACOSX_BUNDLE_BUNDLE_EXECUTABLE_NAME "${PROJECT_NAME}"
+- MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}"
+- MACOSX_BUNDLE_GUI_IDENTIFIER "org.kn4crd.js8call"
+- )
+-
+-target_include_directories (js8call PRIVATE ${FFTW3_INCLUDE_DIRS})
+-if (APPLE)
+- target_link_libraries (js8call wsjt_fort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
+-else ()
+- target_link_libraries (js8call wsjt_fort_omp wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
+- if (OpenMP_C_FLAGS)
+- set_target_properties (js8call PROPERTIES
+- COMPILE_FLAGS "${OpenMP_C_FLAGS}"
+- LINK_FLAGS "${OpenMP_C_FLAGS}"
+- )
+- endif ()
+- set_target_properties (js8call PROPERTIES
+- Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/fortran_modules_omp
+- )
+- if (WIN32)
+- set_target_properties (js8call PROPERTIES
+- LINK_FLAGS -Wl,--stack,16777216
+- )
+- endif ()
+-endif ()
+-qt5_use_modules (js8call SerialPort) # not sure why the interface link library syntax above doesn't work
+-
+ # if (UNIX)
+ # if (NOT WSJT_SKIP_MANPAGES)
+ # add_subdirectory (manpages)
+@@ -976,38 +926,10 @@
+ #
+ # installation
+ #
+-install (TARGETS js8call
+- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
+- BUNDLE DESTINATION . COMPONENT runtime
+- )
+-
+ install (TARGETS js8 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
+ BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
+ )
+
+-install (PROGRAMS
+- ${RIGCTL_EXE}
+- DESTINATION ${CMAKE_INSTALL_BINDIR}
+- #COMPONENT runtime
+- RENAME rigctl-local${CMAKE_EXECUTABLE_SUFFIX}
+- )
+-
+-install (PROGRAMS
+- ${RIGCTLD_EXE}
+- DESTINATION ${CMAKE_INSTALL_BINDIR}
+- #COMPONENT runtime
+- RENAME rigctld-local${CMAKE_EXECUTABLE_SUFFIX}
+- )
+-
+-install (FILES
+- README
+- COPYING
+- INSTALL
+- INSTALL-WSJTX
+- DESTINATION ${CMAKE_INSTALL_DOCDIR}
+- #COMPONENT runtime
+- )
+-
+ install (FILES
+ contrib/Ephemeris/JPLEPH
+ DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}
+@@ -1061,32 +983,6 @@
+ "${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h"
+ )
+
+-
+-if (NOT WIN32 AND NOT APPLE)
+- # install a desktop file so js8call appears in the application start
+- # menu with an icon
+- install (
+- FILES js8call.desktop
+- DESTINATION /usr/share/applications
+- #COMPONENT runtime
+- )
+- install (
+- FILES icons/Unix/js8call_icon.png
+- DESTINATION /usr/share/pixmaps
+- #COMPONENT runtime
+- )
+-
+- IF("${CMAKE_INSTALL_PREFIX}" STREQUAL "/opt/js8call")
+- execute_process(COMMAND ln -s /opt/js8call/bin/js8call ljs8call)
+-
+- install(FILES
+- ${CMAKE_BINARY_DIR}/ljs8call DESTINATION /usr/bin/ RENAME js8call
+- #COMPONENT runtime
+- )
+- endif()
+-endif (NOT WIN32 AND NOT APPLE)
+-
+-
+ #
+ # bundle fixup only done in Release or MinSizeRel configurations
+ #
+Only in js8call/: .idea
diff --git a/docker/files/sdrplay/install-lib.aarch64.patch b/docker/files/sdrplay/install-lib.aarch64.patch
new file mode 120000
index 000000000..2ec83d025
--- /dev/null
+++ b/docker/files/sdrplay/install-lib.aarch64.patch
@@ -0,0 +1 @@
+install-lib.x86_64.patch
\ No newline at end of file
diff --git a/docker/files/sdrplay/install-lib.armv7l.patch b/docker/files/sdrplay/install-lib.armv7l.patch
new file mode 100644
index 000000000..22a78f6cd
--- /dev/null
+++ b/docker/files/sdrplay/install-lib.armv7l.patch
@@ -0,0 +1,40 @@
+diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh
+--- sdrplay-orig/install_lib.sh 2020-05-24 14:13:04.561271707 +0000
++++ sdrplay/install_lib.sh 2020-05-24 14:16:20.068329040 +0000
+@@ -4,19 +4,6 @@
+ MAJVERS="3"
+
+ echo "Installing SDRplay RSP API library ${VERS}..."
+-read -p "Press RETURN to view the license agreement" ret
+-
+-more sdrplay_license.txt
+-
+-while true; do
+- echo "Press y and RETURN to accept the license agreement and continue with"
+- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
+- case $yn in
+- [Yy]* ) break;;
+- [Nn]* ) exit;;
+- * ) echo "Please answer y or n";;
+- esac
+-done
+
+ ARCH=`uname -m`
+
+@@ -141,16 +128,6 @@
+ echo "SDRplay API ${VERS} Installation Finished"
+ echo " "
+
+-while true; do
+- echo "Would you like to add SDRplay USB IDs to the local database for easier
+-"
+- read -p "identification in applications such as lsusb? [y/n] " yn
+- case $yn in
+- [Yy]* ) break;;
+- [Nn]* ) exit;;
+- * ) echo "Please answer y or n";;
+- esac
+-done
+ sudo cp scripts/sdrplay_usbids.sh ${INSTALLBINDIR}/.
+ sudo chmod 755 ${INSTALLBINDIR}/sdrplay_usbids.sh
+ sudo cp scripts/sdrplay_ids.txt ${INSTALLBINDIR}/.
diff --git a/docker/files/sdrplay/install-lib.x86_64.patch b/docker/files/sdrplay/install-lib.x86_64.patch
new file mode 100644
index 000000000..62402f805
--- /dev/null
+++ b/docker/files/sdrplay/install-lib.x86_64.patch
@@ -0,0 +1,148 @@
+diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh
+--- sdrplay-orig/install_lib.sh 2024-02-20 00:57:57.438264040 +0100
++++ sdrplay/install_lib.sh 2024-02-20 01:01:14.293463093 +0100
+@@ -17,26 +17,7 @@
+ echo "the system files."
+ echo " "
+
+-read -p "Press RETURN to view the license agreement" ret
+-more -d sdrplay_license.txt
+-while true; do
+- echo "Press y and RETURN to accept the license agreement and continue with"
+- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
+- case $yn in
+- [Yy]* ) break;;
+- [Nn]* ) exit;;
+- * ) echo "Please answer y or n";;
+- esac
+-done
+-
+-echo " "
+-echo "A copy of the license agreement can be found here: ${HOME}/sdrplay_license.txt"
+-cp sdrplay_license.txt ${HOME}/.
+-chmod 644 ${HOME}/sdrplay_license.txt
+-echo " "
+-
+ ARCH=$(uname -m|sed -e 's/x86_64/64/' -e 's/aarch64/64/' -e 's/arm64/64/' -e 's/i.86/32/')
+-INIT=$(file -L /sbin/init|sed -e 's/^.* \(32\|64\)-bit.*$/\1/')
+ COMPILER=$(getconf LONG_BIT)
+ ARCHM=$(uname -m)
+ INSTALLARCH=$(uname -m)
+@@ -47,12 +28,11 @@
+
+ echo " "
+ echo "Architecture reported as being $ARCH bit"
+-echo "System reports $INIT bit files found"
+ echo "System is also setup to produce $COMPILER bit files"
+ echo "Architecture reports machine as being $ARCHM compliant"
+ echo " "
+
+-if [ "${ARCH}" != "64" ] || [ "${INIT}" != "64" ] || [ "${COMPILER}" != "64" ]; then
++if [ "${ARCH}" != "64" ] || [ "${COMPILER}" != "64" ]; then
+ echo "This installer only supports 64 bit architectures."
+ echo "One of the above indicates that something is not set for"
+ echo "64 bit operation. Please either fix the relevant OS issue or"
+@@ -194,11 +174,6 @@
+ sudo chmod 644 /etc/udev/hwdb.d/20-sdrplay.hwdb
+ sudo systemd-hwdb update
+ sudo udevadm trigger
+- if [ "${SRVTYPE}" != "initd" ]; then
+- sudo systemctl restart udev
+- else
+- sudo service udev restart
+- fi
+ echo "Done"
+ fi
+ else
+@@ -234,7 +209,7 @@
+ fi
+
+ echo " "
+-locservice="/opt/sdrplay_api"
++locservice="/usr/local/bin"
+ locheader="/usr/local/include"
+ loclib="/usr/local/lib"
+ locscripts="/etc/systemd/system"
+@@ -254,45 +229,6 @@
+ echo "Daemon start system : ${DAEMON_SYS}"
+ echo " "
+
+-# 0--------1---------2---------3---------4---------5---------6---------7---------8
+-while true; do
+- echo "To continue the installation with these defaults press y and RETURN"
+- read -p "or press n and RETURN to change them [y/n] " yn
+- case $yn in
+- [Yy]* ) change="n";break;;
+- [Nn]* ) change="y";break;;
+- * ) echo "Please answer y or n";;
+- esac
+-done
+-
+-if [ "${change}" == "y" ]; then
+- echo "Changing default locations..."
+- read -p "API service location [${locservice}]: " newloc
+- if [ "${newloc}" != "" ]; then
+- locservice=${newloc}
+- fi
+- read -p "API header files location [${locheader}]: " newloc
+- if [ "${newloc}" != "" ]; then
+- locheader=${newloc}
+- fi
+- read -p "API shared library location [${loclib}]: " newloc
+- if [ "${newloc}" != "" ]; then
+- loclib=${newloc}
+- fi
+-
+- echo "API service : ${locservice}"
+- echo "API header files : ${locheader}"
+- echo "API shared library : ${loclib}"
+- while true; do
+- read -p "Please confirm these are correct [y/n] " yn
+- case $yn in
+- [Yy]* ) break;;
+- [Nn]* ) echo "paths not confirmed. Exiting...";exit 1;;
+- * ) echo "Please answer y or n";;
+- esac
+- done
+-fi
+-
+ sudo mkdir -p -m 755 ${locservice} >> /dev/null 2>&1
+ sudo mkdir -p -m 755 ${locheader} >> /dev/null 2>&1
+ sudo mkdir -p -m 755 ${loclib} >> /dev/null 2>&1
+@@ -324,10 +260,6 @@
+ echo -n "Installing Service scripts and starting daemon..."
+ if [ -d "/etc/systemd/system" ]; then
+ SRVTYPE="systemd"
+- if [ -f "/etc/systemd/system/sdrplay.service" ]; then
+- sudo systemctl stop sdrplay
+- sudo systemctl disable sdrplay
+- fi
+ sudo bash -c 'cat > /etc/systemd/system/sdrplay.service' << EOF
+ [Unit]
+ Description=SDRplay API Service
+@@ -346,8 +278,6 @@
+ EOF
+
+ sudo chmod 644 /etc/systemd/system/sdrplay.service
+- sudo systemctl enable sdrplay
+- sudo systemctl start sdrplay
+ else
+ SRVTYPE="initd"
+ if [ -f "/etc/init.d/sdrplayService" ]; then
+@@ -450,16 +380,6 @@
+ echo "finished, please reboot this device."
+
+ echo " "
+-echo "To start and stop the API service, use the following commands..."
+-echo " "
+-if [ "${SRVTYPE}" != "systemd" ]; then
+- echo "sudo service sdrplayService start"
+- echo "sudo service sdrplayService stop"
+-else
+- echo "sudo systemctl start sdrplay"
+- echo "sudo systemctl stop sdrplay"
+-fi
+-echo " "
+ echo "If supported on your system, lsusb will now show the RSP name"
+ echo " "
+ echo "SDRplay API ${VERS} Installation Finished"
diff --git a/docker/files/services/codecserver/run b/docker/files/services/codecserver/run
new file mode 100755
index 000000000..13b7872ea
--- /dev/null
+++ b/docker/files/services/codecserver/run
@@ -0,0 +1,2 @@
+#!/command/execlineb -P
+/usr/local/bin/codecserver
\ No newline at end of file
diff --git a/docker/files/services/sdrplay/run b/docker/files/services/sdrplay/run
new file mode 100755
index 000000000..cc8b6a7d4
--- /dev/null
+++ b/docker/files/services/sdrplay/run
@@ -0,0 +1,2 @@
+#!/command/execlineb -P
+/usr/local/bin/sdrplay_apiService
\ No newline at end of file
diff --git a/docker/files/wsjtx/wsjtx-hamlib.patch b/docker/files/wsjtx/wsjtx-hamlib.patch
new file mode 100644
index 000000000..a4e277d10
--- /dev/null
+++ b/docker/files/wsjtx/wsjtx-hamlib.patch
@@ -0,0 +1,50 @@
+--- CMakeLists.txt.orig 2021-09-28 14:33:14.329598412 +0200
++++ CMakeLists.txt 2021-09-28 14:34:23.052345270 +0200
+@@ -106,24 +106,6 @@
+
+
+ #
+-# build and install hamlib locally so it can be referenced by the
+-# WSJT-X build
+-#
+-ExternalProject_Add (hamlib
+- GIT_REPOSITORY ${hamlib_repo}
+- GIT_TAG ${hamlib_TAG}
+- GIT_SHALLOW False
+- URL ${CMAKE_CURRENT_SOURCE_DIR}/src/${__hamlib_upstream}.tar.gz
+- URL_HASH MD5=${hamlib_md5sum}
+- #UPDATE_COMMAND ${CMAKE_COMMAND} -E env "[ -f ./bootstrap ] && ./bootstrap"
+- PATCH_COMMAND ${PATCH_EXECUTABLE} -p1 -N < ${CMAKE_CURRENT_SOURCE_DIR}/hamlib.patch
+- CONFIGURE_COMMAND /configure --prefix= --disable-shared --enable-static --without-cxx-binding ${EXTRA_FLAGS} # LIBUSB_LIBS=${USB_LIBRARY}
+- BUILD_COMMAND $(MAKE) all V=1 # $(MAKE) is ExternalProject_Add() magic to do recursive make
+- INSTALL_COMMAND $(MAKE) install-strip V=1 DESTDIR=""
+- STEP_TARGETS update install
+- )
+-
+-#
+ # custom target to make a hamlib source tarball
+ #
+ add_custom_target (hamlib_sources
+@@ -161,7 +143,6 @@
+ # build and optionally install WSJT-X using the hamlib package built
+ # above
+ #
+-ExternalProject_Get_Property (hamlib INSTALL_DIR)
+ ExternalProject_Add (wsjtx
+ GIT_REPOSITORY ${wsjtx_repo}
+ GIT_TAG ${WSJTX_TAG}
+@@ -186,14 +167,8 @@
+ DEPENDEES build
+ )
+
+-set_target_properties (hamlib PROPERTIES EXCLUDE_FROM_ALL 1)
+ set_target_properties (wsjtx PROPERTIES EXCLUDE_FROM_ALL 1)
+
+-add_dependencies (wsjtx-configure hamlib-install)
+-add_dependencies (wsjtx-build hamlib-install)
+-add_dependencies (wsjtx-install hamlib-install)
+-add_dependencies (wsjtx-package hamlib-install)
+-
+ # export traditional targets
+ add_custom_target (build ALL DEPENDS wsjtx-build)
+ add_custom_target (install DEPENDS wsjtx-install)
diff --git a/docker/files/wsjtx/wsjtx.patch b/docker/files/wsjtx/wsjtx.patch
new file mode 100644
index 000000000..75ce3a6c4
--- /dev/null
+++ b/docker/files/wsjtx/wsjtx.patch
@@ -0,0 +1,341 @@
+diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
+--- wsjtx-orig/CMakeLists.txt 2023-01-28 17:43:05.586124507 +0100
++++ wsjtx/CMakeLists.txt 2023-01-28 17:56:07.108634912 +0100
+@@ -122,7 +122,7 @@
+ option (WSJT_QDEBUG_TO_FILE "Redirect Qt debuging messages to a trace file.")
+ option (WSJT_SOFT_KEYING "Apply a ramp to CW keying envelope to reduce transients." ON)
+ option (WSJT_SKIP_MANPAGES "Skip *nix manpage generation.")
+-option (WSJT_GENERATE_DOCS "Generate documentation files." ON)
++option (WSJT_GENERATE_DOCS "Generate documentation files.")
+ option (WSJT_RIG_NONE_CAN_SPLIT "Allow split operation with \"None\" as rig.")
+ option (WSJT_TRACE_UDP "Debugging option that turns on UDP message protocol diagnostics.")
+ option (WSJT_BUILD_UTILS "Build simulators and code demonstrators." ON)
+@@ -170,77 +170,7 @@
+ )
+
+ set (wsjt_qt_CXXSRCS
+- helper_functions.cpp
+- qt_helpers.cpp
+- widgets/MessageBox.cpp
+- MetaDataRegistry.cpp
+- Network/NetworkServerLookup.cpp
+ revision_utils.cpp
+- L10nLoader.cpp
+- WFPalette.cpp
+- Radio.cpp
+- RadioMetaType.cpp
+- NonInheritingProcess.cpp
+- models/IARURegions.cpp
+- models/Bands.cpp
+- models/Modes.cpp
+- models/FrequencyList.cpp
+- models/StationList.cpp
+- widgets/FrequencyLineEdit.cpp
+- widgets/FrequencyDeltaLineEdit.cpp
+- item_delegates/CandidateKeyFilter.cpp
+- item_delegates/ForeignKeyDelegate.cpp
+- item_delegates/MessageItemDelegate.cpp
+- validators/LiveFrequencyValidator.cpp
+- GetUserId.cpp
+- Audio/AudioDevice.cpp
+- Transceiver/Transceiver.cpp
+- Transceiver/TransceiverBase.cpp
+- Transceiver/EmulateSplitTransceiver.cpp
+- Transceiver/TransceiverFactory.cpp
+- Transceiver/PollingTransceiver.cpp
+- Transceiver/HamlibTransceiver.cpp
+- Transceiver/HRDTransceiver.cpp
+- Transceiver/DXLabSuiteCommanderTransceiver.cpp
+- Network/NetworkMessage.cpp
+- Network/MessageClient.cpp
+- widgets/LettersSpinBox.cpp
+- widgets/HintedSpinBox.cpp
+- widgets/RestrictedSpinBox.cpp
+- widgets/HelpTextWindow.cpp
+- SampleDownloader.cpp
+- SampleDownloader/DirectoryDelegate.cpp
+- SampleDownloader/Directory.cpp
+- SampleDownloader/FileNode.cpp
+- SampleDownloader/RemoteFile.cpp
+- DisplayManual.cpp
+- MultiSettings.cpp
+- validators/MaidenheadLocatorValidator.cpp
+- validators/CallsignValidator.cpp
+- widgets/SplashScreen.cpp
+- EqualizationToolsDialog.cpp
+- widgets/DoubleClickablePushButton.cpp
+- widgets/DoubleClickableRadioButton.cpp
+- Network/LotWUsers.cpp
+- models/DecodeHighlightingModel.cpp
+- widgets/DecodeHighlightingListView.cpp
+- models/FoxLog.cpp
+- widgets/AbstractLogWindow.cpp
+- widgets/FoxLogWindow.cpp
+- widgets/CabrilloLogWindow.cpp
+- item_delegates/CallsignDelegate.cpp
+- item_delegates/MaidenheadLocatorDelegate.cpp
+- item_delegates/FrequencyDelegate.cpp
+- item_delegates/FrequencyDeltaDelegate.cpp
+- item_delegates/SQLiteDateTimeDelegate.cpp
+- models/CabrilloLog.cpp
+- logbook/AD1CCty.cpp
+- logbook/WorkedBefore.cpp
+- logbook/Multiplier.cpp
+- Network/NetworkAccessManager.cpp
+- widgets/LazyFillComboBox.cpp
+- widgets/CheckableItemComboBox.cpp
+- widgets/BandComboBox.cpp
+ )
+
+ set (wsjt_qtmm_CXXSRCS
+@@ -1089,9 +1019,6 @@
+ if (WSJT_GENERATE_DOCS)
+ add_subdirectory (doc)
+ endif (WSJT_GENERATE_DOCS)
+-if (EXISTS ${CMAKE_SOURCE_DIR}/tests AND IS_DIRECTORY ${CMAKE_SOURCE_DIR}/tests)
+- add_subdirectory (tests)
+-endif ()
+
+ # build a library of package functionality (without and optionally with OpenMP support)
+ add_library (wsjt_cxx STATIC ${wsjt_CSRCS} ${wsjt_CXXSRCS})
+@@ -1357,10 +1284,7 @@
+ add_library (wsjt_qt STATIC ${wsjt_qt_CXXSRCS} ${wsjt_qt_GENUISRCS} ${GENAXSRCS})
+ # set wsjtx_udp exports to static variants
+ target_compile_definitions (wsjt_qt PUBLIC UDP_STATIC_DEFINE)
+-target_link_libraries (wsjt_qt Hamlib::Hamlib Boost::log qcp Qt5::Widgets Qt5::Network Qt5::Sql)
+-if (WIN32)
+- target_link_libraries (wsjt_qt Qt5::AxContainer Qt5::AxBase)
+-endif (WIN32)
++target_link_libraries (wsjt_qt Qt5::Core)
+
+ # build a library of package Qt functionality used in Fortran utilities
+ add_library (fort_qt STATIC ${fort_qt_CXXSRCS})
+@@ -1425,90 +1349,6 @@
+ add_subdirectory (map65)
+ endif ()
+
+-# build the main application
+-generate_version_info (wsjtx_VERSION_RESOURCES
+- NAME wsjtx
+- BUNDLE ${PROJECT_BUNDLE_NAME}
+- ICON ${WSJTX_ICON_FILE}
+- )
+-
+-add_executable (wsjtx MACOSX_BUNDLE
+- ${wsjtx_CXXSRCS}
+- ${wsjtx_GENUISRCS}
+- ${WSJTX_ICON_FILE}
+- ${wsjtx_RESOURCES_RCC}
+- ${wsjtx_VERSION_RESOURCES}
+- )
+-
+-if (WSJT_CREATE_WINMAIN)
+- set_target_properties (wsjtx PROPERTIES WIN32_EXECUTABLE ON)
+-endif (WSJT_CREATE_WINMAIN)
+-
+-set_target_properties (wsjtx PROPERTIES
+- MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Darwin/Info.plist.in"
+- MACOSX_BUNDLE_INFO_STRING "${PROJECT_DESCRIPTION}"
+- MACOSX_BUNDLE_ICON_FILE "${WSJTX_ICON_FILE}"
+- MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}
+- MACOSX_BUNDLE_SHORT_VERSION_STRING "v${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}"
+- MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${SCS_VERSION_STR}"
+- MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_BUNDLE_NAME}"
+- MACOSX_BUNDLE_BUNDLE_EXECUTABLE_NAME "${PROJECT_NAME}"
+- MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}"
+- MACOSX_BUNDLE_GUI_IDENTIFIER "org.k1jt.wsjtx"
+- )
+-
+-target_include_directories (wsjtx PRIVATE ${FFTW3_INCLUDE_DIRS})
+-if ((NOT ${OPENMP_FOUND}) OR APPLE)
+- target_link_libraries (wsjtx wsjt_fort)
+-else ()
+- target_link_libraries (wsjtx wsjt_fort_omp)
+- if (OpenMP_C_FLAGS)
+- set_target_properties (wsjtx PROPERTIES
+- COMPILE_FLAGS "${OpenMP_C_FLAGS}"
+- LINK_FLAGS "${OpenMP_C_FLAGS}"
+- )
+- endif ()
+- set_target_properties (wsjtx PROPERTIES
+- Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/fortran_modules_omp
+- )
+- if (WIN32)
+- set_target_properties (wsjtx PROPERTIES
+- LINK_FLAGS -Wl,--stack,0x1000000,--heap,0x20000000
+- )
+- endif ()
+-endif ()
+-target_link_libraries (wsjtx Qt5::SerialPort wsjt_cxx wsjt_qt wsjt_qtmm ${FFTW3_LIBRARIES} ${LIBM_LIBRARIES})
+-
+-# make a library for WSJT-X UDP servers
+-# add_library (wsjtx_udp SHARED ${UDP_library_CXXSRCS})
+-add_library (wsjtx_udp-static STATIC ${UDP_library_CXXSRCS})
+-#target_include_directories (wsjtx_udp
+-# INTERFACE
+-# $
+-# )
+-target_include_directories (wsjtx_udp-static
+- INTERFACE
+- $
+- )
+-#set_target_properties (wsjtx_udp PROPERTIES
+-# PUBLIC_HEADER "${UDP_library_HEADERS}"
+-# )
+-set_target_properties (wsjtx_udp-static PROPERTIES
+- OUTPUT_NAME wsjtx_udp
+- )
+-target_compile_definitions (wsjtx_udp-static PUBLIC UDP_STATIC_DEFINE)
+-target_link_libraries (wsjtx_udp-static Qt5::Network Qt5::Gui)
+-generate_export_header (wsjtx_udp-static BASE_NAME udp)
+-
+-generate_version_info (udp_daemon_VERSION_RESOURCES
+- NAME udp_daemon
+- BUNDLE ${PROJECT_BUNDLE_NAME}
+- ICON ${WSJTX_ICON_FILE}
+- FILE_DESCRIPTION "Example WSJT-X UDP Message Protocol daemon"
+- )
+-add_executable (udp_daemon UDPExamples/UDPDaemon.cpp ${udp_daemon_VERSION_RESOURCES})
+-target_link_libraries (udp_daemon wsjtx_udp-static)
+-
+ generate_version_info (wsjtx_app_version_VERSION_RESOURCES
+ NAME wsjtx_app_version
+ BUNDLE ${PROJECT_BUNDLE_NAME}
+@@ -1518,47 +1358,9 @@
+ add_executable (wsjtx_app_version AppVersion/AppVersion.cpp ${wsjtx_app_version_VERSION_RESOURCES})
+ target_link_libraries (wsjtx_app_version wsjt_qt)
+
+-generate_version_info (message_aggregator_VERSION_RESOURCES
+- NAME message_aggregator
+- BUNDLE ${PROJECT_BUNDLE_NAME}
+- ICON ${WSJTX_ICON_FILE}
+- FILE_DESCRIPTION "Example WSJT-X UDP Message Protocol application"
+- )
+-add_resources (message_aggregator_RESOURCES /qss ${message_aggregator_STYLESHEETS})
+-configure_file (UDPExamples/message_aggregator.qrc.in message_aggregator.qrc @ONLY)
+-qt5_add_resources (message_aggregator_RESOURCES_RCC
+- ${CMAKE_CURRENT_BINARY_DIR}/message_aggregator.qrc
+- contrib/QDarkStyleSheet/qdarkstyle/style.qrc
+- )
+-add_executable (message_aggregator
+- ${message_aggregator_CXXSRCS}
+- ${message_aggregator_RESOURCES_RCC}
+- ${message_aggregator_VERSION_RESOURCES}
+- )
+-target_link_libraries (message_aggregator wsjt_qt Qt5::Widgets wsjtx_udp-static)
+-
+-if (WSJT_CREATE_WINMAIN)
+- set_target_properties (message_aggregator PROPERTIES WIN32_EXECUTABLE ON)
+-endif (WSJT_CREATE_WINMAIN)
+-
+-if (UNIX)
+- if (NOT WSJT_SKIP_MANPAGES)
+- add_subdirectory (manpages)
+- add_dependencies (wsjtx manpages)
+- endif (NOT WSJT_SKIP_MANPAGES)
+- if (NOT APPLE)
+- add_subdirectory (debian)
+- add_dependencies (wsjtx debian)
+- endif (NOT APPLE)
+-endif (UNIX)
+-
+ #
+ # installation
+ #
+-install (TARGETS wsjtx
+- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
+- BUNDLE DESTINATION . COMPONENT runtime
+- )
+
+ # install (TARGETS wsjtx_udp EXPORT udp
+ # RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+@@ -1577,12 +1379,7 @@
+ # DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx
+ # )
+
+-install (TARGETS udp_daemon message_aggregator wsjtx_app_version
+- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
+- BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
+- )
+-
+-install (TARGETS jt9 wsprd fmtave fcal fmeasure
++install (TARGETS wsjtx_app_version jt9 wsprd
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
+ BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
+ )
+@@ -1595,38 +1392,6 @@
+ )
+ endif(WSJT_BUILD_UTILS)
+
+-install (PROGRAMS
+- ${RIGCTL_EXE}
+- DESTINATION ${CMAKE_INSTALL_BINDIR}
+- #COMPONENT runtime
+- RENAME rigctl-wsjtx${CMAKE_EXECUTABLE_SUFFIX}
+- )
+-
+-install (PROGRAMS
+- ${RIGCTLD_EXE}
+- DESTINATION ${CMAKE_INSTALL_BINDIR}
+- #COMPONENT runtime
+- RENAME rigctld-wsjtx${CMAKE_EXECUTABLE_SUFFIX}
+- )
+-
+-install (PROGRAMS
+- ${RIGCTLCOM_EXE}
+- DESTINATION ${CMAKE_INSTALL_BINDIR}
+- #COMPONENT runtime
+- RENAME rigctlcom-wsjtx${CMAKE_EXECUTABLE_SUFFIX}
+- )
+-
+-install (FILES
+- README
+- COPYING
+- AUTHORS
+- THANKS
+- NEWS
+- BUGS
+- DESTINATION ${CMAKE_INSTALL_DOCDIR}
+- #COMPONENT runtime
+- )
+-
+ install (FILES
+ cty.dat
+ cty.dat_copyright.txt
+@@ -1635,13 +1400,6 @@
+ #COMPONENT runtime
+ )
+
+-install (DIRECTORY
+- example_log_configurations
+- DESTINATION ${CMAKE_INSTALL_DOCDIR}
+- FILES_MATCHING REGEX "^.*[^~]$"
+- #COMPONENT runtime
+- )
+-
+ #
+ # Mac installer files
+ #
+@@ -1693,22 +1451,6 @@
+ "${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h"
+ )
+
+-
+-if (NOT WIN32 AND NOT APPLE)
+- # install a desktop file so wsjtx appears in the application start
+- # menu with an icon
+- install (
+- FILES wsjtx.desktop message_aggregator.desktop
+- DESTINATION share/applications
+- #COMPONENT runtime
+- )
+- install (
+- FILES icons/Unix/wsjtx_icon.png
+- DESTINATION share/pixmaps
+- #COMPONENT runtime
+- )
+-endif (NOT WIN32 AND NOT APPLE)
+-
+ if (APPLE)
+ set (CMAKE_POSTFLIGHT_SCRIPT
+ "${wsjtx_BINARY_DIR}/postflight.sh")
diff --git a/docker/scripts/install-connectors.sh b/docker/scripts/install-connectors.sh
new file mode 100755
index 000000000..f056f2978
--- /dev/null
+++ b/docker/scripts/install-connectors.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+set -euxo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ mkdir build
+ cd build
+ cmake ..
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES="libfftw3-single3"
+BUILD_PACKAGES="git cmake make gcc g++ libsamplerate-dev libfftw3-dev"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+git clone https://github.com/jketterl/owrx_connector.git
+# latest develop as of 2024-01-01 (fixed startup race condition)
+cmakebuild owrx_connector 62219d40e180abb539ad61fcd9625b90c34f0e26
+
+apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-afedri.sh b/docker/scripts/install-dependencies-afedri.sh
new file mode 100755
index 000000000..f71e0743d
--- /dev/null
+++ b/docker/scripts/install-dependencies-afedri.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+set -euo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ mkdir build
+ cd build
+ cmake ..
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES=""
+BUILD_PACKAGES="git cmake make gcc g++"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+git clone https://github.com/alexander-sholohov/SoapyAfedri.git
+cmakebuild SoapyAfedri 1.0.1
+
+apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-airspy.sh b/docker/scripts/install-dependencies-airspy.sh
new file mode 100755
index 000000000..72032bace
--- /dev/null
+++ b/docker/scripts/install-dependencies-airspy.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+set -euxo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ mkdir build
+ cd build
+ cmake ..
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES="libusb-1.0-0"
+BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+git clone https://github.com/airspy/airspyone_host.git
+# latest from master as of 2020-09-04
+cmakebuild airspyone_host 652fd7f1a8f85687641e0bd91f739694d7258ecc
+
+git clone https://github.com/pothosware/SoapyAirspy.git
+cmakebuild SoapyAirspy 10d697b209e7f1acc8b2c8d24851d46170ef77e3
+
+git clone https://github.com/airspy/airspyhf.git
+# latest from master as of 2020-09-04
+cmakebuild airspyhf 8891387edddcd185e2949e9814e9ef35f46f0722
+
+git clone https://github.com/pothosware/SoapyAirspyHF.git
+# latest from master as of 2020-09-04
+cmakebuild SoapyAirspyHF 5488dac5b44f1432ce67b40b915f7e61d3bd4853
+
+apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-bladerf.sh b/docker/scripts/install-dependencies-bladerf.sh
new file mode 100755
index 000000000..2b2e53205
--- /dev/null
+++ b/docker/scripts/install-dependencies-bladerf.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+set -euxo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ mkdir build
+ cd build
+ cmake ..
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES="libusb-1.0-0"
+BUILD_PACKAGES="git cmake make gcc g++ libusb-1.0-0-dev"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+git clone https://github.com/Nuand/bladeRF.git
+cmakebuild bladeRF 2023.02
+
+git clone https://github.com/pothosware/SoapyBladeRF.git
+# latest from master as of 2023-08-30
+cmakebuild SoapyBladeRF 85f6dc554ed4c618304d99395b19c4e1523675b0
+
+apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-fcdpp.sh b/docker/scripts/install-dependencies-fcdpp.sh
new file mode 100755
index 000000000..49f14394d
--- /dev/null
+++ b/docker/scripts/install-dependencies-fcdpp.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+set -euxo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ mkdir build
+ cd build
+ cmake ..
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES="libhidapi-hidraw0 libhidapi-libusb0 libasound2"
+BUILD_PACKAGES="git cmake make gcc g++ libhidapi-dev libasound2-dev"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+git clone https://github.com/pothosware/SoapyFCDPP.git
+cmakebuild SoapyFCDPP soapy-fcdpp-0.1.1
+
+apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-hackrf.sh b/docker/scripts/install-dependencies-hackrf.sh
new file mode 100755
index 000000000..19a458a33
--- /dev/null
+++ b/docker/scripts/install-dependencies-hackrf.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+set -euxo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ mkdir build
+ cd build
+ cmake ..
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES="libusb-1.0-0 libfftw3-single3 udev"
+BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev libfftw3-dev pkg-config"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+git clone https://github.com/mossmann/hackrf.git
+cd hackrf
+# latest from master as of 2020-09-04
+git checkout 6e5cbda2945c3bab0e6e1510eae418eda60c358e
+cmakebuild host
+cd ..
+rm -rf hackrf
+
+git clone https://github.com/pothosware/SoapyHackRF.git
+# latest from master as of 2020-09-04
+cmakebuild SoapyHackRF 7d530872f96c1cbe0ed62617c32c48ce7e103e1d
+
+SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-hpsdr.sh b/docker/scripts/install-dependencies-hpsdr.sh
new file mode 100755
index 000000000..24dd1e64e
--- /dev/null
+++ b/docker/scripts/install-dependencies-hpsdr.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+set -euxo pipefail
+export MAKEFLAGS="-j4"
+
+BUILD_PACKAGES="git wget gcc libc6-dev"
+
+apt-get update
+apt-get -y install --no-install-recommends $BUILD_PACKAGES
+
+pushd /tmp
+
+ARCH=$(uname -m)
+GOVERSION=1.20.10
+
+case ${ARCH} in
+ x86_64)
+ PACKAGE=go${GOVERSION}.linux-amd64.tar.gz
+ ;;
+ armv*)
+ PACKAGE=go${GOVERSION}.linux-armv6l.tar.gz
+ ;;
+ aarch64)
+ PACKAGE=go${GOVERSION}.linux-arm64.tar.gz
+ ;;
+esac
+
+wget https://golang.org/dl/${PACKAGE}
+tar xfz $PACKAGE
+
+git clone https://github.com/jancona/hpsdrconnector.git
+pushd hpsdrconnector
+git checkout v0.6.4
+/tmp/go/bin/go build
+install -m 0755 hpsdrconnector /usr/local/bin
+
+popd
+
+rm -rf hpsdrconnector
+rm -rf go
+rm $PACKAGE
+
+popd
+
+apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-limesdr.sh b/docker/scripts/install-dependencies-limesdr.sh
new file mode 100755
index 000000000..4f83298ef
--- /dev/null
+++ b/docker/scripts/install-dependencies-limesdr.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+set -euo pipefail
+export MAKEFLAGS="-j4"
+
+cd /tmp
+
+STATIC_PACKAGES="libusb-1.0-0 libatomic1"
+BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+SIMD_FLAGS=""
+if [[ 'x86_64' == `uname -m` ]] ; then
+ SIMD_FLAGS="-DDEFAULT_SIMD_FLAGS=SSE3"
+fi
+
+git clone https://github.com/myriadrf/LimeSuite.git
+cd LimeSuite
+# latest from master as of 2020-09-04
+git checkout 9526621f8b4c9e2a7f638b5ef50c45560dcad22a
+mkdir builddir
+cd builddir
+cmake .. -DENABLE_EXAMPLES=OFF -DENABLE_DESKTOP=OFF -DENABLE_LIME_UTIL=OFF -DENABLE_QUICKTEST=OFF -DENABLE_OCTAVE=OFF -DENABLE_GUI=OFF -DCMAKE_CXX_STANDARD_LIBRARIES="-latomic" ${SIMD_FLAGS}
+make
+make install
+cd ../..
+rm -rf LimeSuite
+
+apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-perseus.sh b/docker/scripts/install-dependencies-perseus.sh
new file mode 100755
index 000000000..1d8f1c977
--- /dev/null
+++ b/docker/scripts/install-dependencies-perseus.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+set -euxo pipefail
+export MAKEFLAGS="-j4"
+
+cd /tmp
+
+STATIC_PACKAGES="libusb-1.0-0 libudev1"
+BUILD_PACKAGES="git make gcc autoconf automake libtool libusb-1.0-0-dev xxd"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+git clone https://github.com/Microtelecom/libperseus-sdr.git
+cd libperseus-sdr
+# latest from master as of 2020-09-04
+git checkout c2c95daeaa08bf0daed0e8ada970ab17cc264e1b
+./bootstrap.sh
+./configure
+make
+make install
+ldconfig /etc/ld.so.conf.d
+cd ..
+rm -rf libperseus-sdr
+
+apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-plutosdr.sh b/docker/scripts/install-dependencies-plutosdr.sh
new file mode 100755
index 000000000..aa801b560
--- /dev/null
+++ b/docker/scripts/install-dependencies-plutosdr.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+set -euo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ mkdir build
+ cd build
+ cmake .. ${3:-}
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES="libusb-1.0-0 libxml2"
+BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ libxml2-dev flex bison pkg-config"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+git clone https://github.com/analogdevicesinc/libiio.git
+cmakebuild libiio v0.21 -DCMAKE_INSTALL_PREFIX=/usr/local
+
+git clone https://github.com/analogdevicesinc/libad9361-iio.git
+cmakebuild libad9361-iio v0.2
+
+git clone https://github.com/pothosware/SoapyPlutoSDR.git
+# latest from master as of 2020-09-04
+cmakebuild SoapyPlutoSDR 93717b32ef052e0dfa717aa2c1a4eb27af16111f
+
+apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-radioberry.sh b/docker/scripts/install-dependencies-radioberry.sh
new file mode 100755
index 000000000..44688e0d6
--- /dev/null
+++ b/docker/scripts/install-dependencies-radioberry.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+set -euxo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ mkdir build
+ cd build
+ cmake ..
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES=""
+BUILD_PACKAGES="git cmake make gcc g++"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+git clone https://github.com/pa3gsb/Radioberry-2.x
+cd Radioberry-2.x/SBC/rpi-4
+
+# latest from master as of 2020-09-04
+cmakebuild SoapyRadioberrySDR 8d17de6b4dc076e628900a82f05c7cf0b16cbe24
+cd ../../..
+rm -rf Radioberry-2.x
+
+SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-rtlsdr-soapy.sh b/docker/scripts/install-dependencies-rtlsdr-soapy.sh
new file mode 100755
index 000000000..79077147f
--- /dev/null
+++ b/docker/scripts/install-dependencies-rtlsdr-soapy.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+set -euo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ mkdir build
+ cd build
+ cmake ..
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES="libusb-1.0-0"
+BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+git clone https://github.com/osmocom/rtl-sdr.git
+cmakebuild rtl-sdr v2.0.1
+
+git clone https://github.com/pothosware/SoapyRTLSDR.git
+# latest from master as of 2023-09-13
+cmakebuild SoapyRTLSDR 068aa77a4c938b239c9d80cd42c4ee7986458e8f
+
+apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-rtlsdr.sh b/docker/scripts/install-dependencies-rtlsdr.sh
new file mode 100755
index 000000000..ccaa53827
--- /dev/null
+++ b/docker/scripts/install-dependencies-rtlsdr.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+set -euxo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ mkdir build
+ cd build
+ cmake ..
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES="libusb-1.0.0"
+BUILD_PACKAGES="git libusb-1.0.0-dev cmake make gcc g++ pkg-config"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+git clone https://github.com/osmocom/rtl-sdr.git
+cmakebuild rtl-sdr v2.0.1
+
+apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-runds.sh b/docker/scripts/install-dependencies-runds.sh
new file mode 100755
index 000000000..fdf263014
--- /dev/null
+++ b/docker/scripts/install-dependencies-runds.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+set -euxo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ mkdir build
+ cd build
+ cmake ..
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES="libfftw3-single3"
+BUILD_PACKAGES="git cmake make gcc g++ pkg-config libfftw3-dev"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+git clone https://github.com/jketterl/runds_connector.git
+# latest develop as of 2023-07-04 (cmake exports)
+cmakebuild runds_connector 435364002d756735015707e7f59aa40e8d743585
+
+apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-sdrplay.sh b/docker/scripts/install-dependencies-sdrplay.sh
new file mode 100755
index 000000000..d09cb9119
--- /dev/null
+++ b/docker/scripts/install-dependencies-sdrplay.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+set -euxo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ mkdir build
+ cd build
+ cmake ..
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES="libusb-1.0.0 udev"
+BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+ARCH=$(uname -m)
+
+case $ARCH in
+ x86_64|aarch64)
+ BINARY=SDRplay_RSP_API-Linux-3.14.0.run
+ ;;
+ armv*)
+ BINARY=SDRplay_RSP_API-ARM32-3.07.2.run
+ ;;
+esac
+
+wget --no-http-keep-alive https://www.sdrplay.com/software/$BINARY
+sh $BINARY --noexec --target sdrplay
+patch --verbose -Np0 < /install-lib.$ARCH.patch
+
+cd sdrplay
+./install_lib.sh
+cd ..
+rm -rf sdrplay
+rm $BINARY
+
+git clone https://github.com/pothosware/SoapySDRPlay3.git
+# latest from master as of 2021-06-19 (reliability fixes)
+cmakebuild SoapySDRPlay3 a869f25364a1f0d5b16169ff908aa21a2ace475d
+
+SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-soapyremote.sh b/docker/scripts/install-dependencies-soapyremote.sh
new file mode 100755
index 000000000..a74c46520
--- /dev/null
+++ b/docker/scripts/install-dependencies-soapyremote.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+set -euo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ mkdir build
+ cd build
+ cmake ..
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES="avahi-daemon libavahi-client3"
+BUILD_PACKAGES="git cmake make gcc g++ libavahi-client-dev"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+git clone https://github.com/pothosware/SoapyRemote.git
+cmakebuild SoapyRemote soapy-remote-0.5.2
+
+apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-soapysdr.sh b/docker/scripts/install-dependencies-soapysdr.sh
new file mode 100755
index 000000000..bd312b440
--- /dev/null
+++ b/docker/scripts/install-dependencies-soapysdr.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+set -euxo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ mkdir build
+ cd build
+ cmake ..
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES="libudev1"
+BUILD_PACKAGES="git cmake make patch wget sudo gcc g++"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+git clone https://github.com/pothosware/SoapySDR
+# latest from master as of 2020-09-04
+cmakebuild SoapySDR 580b94f3dad46899f34ec0a060dbb4534e844e57
+
+SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies-uhd.sh b/docker/scripts/install-dependencies-uhd.sh
new file mode 100755
index 000000000..87a5732cc
--- /dev/null
+++ b/docker/scripts/install-dependencies-uhd.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+set -euo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ mkdir build
+ cd build
+ cmake ..
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES="libusb-1.0.0 libboost-chrono1.74.0 libboost-date-time1.74.0 libboost-filesystem1.74.0 libboost-program-options1.74.0 libboost-regex1.74.0 libboost-test1.74.0 libboost-serialization1.74.0 libboost-thread1.74.0 libboost-system1.74.0 python3-numpy python3-mako"
+BUILD_PACKAGES="git cmake make gcc g++ libusb-1.0-0-dev libboost-dev libboost-chrono-dev libboost-date-time-dev libboost-filesystem-dev libboost-program-options-dev libboost-regex-dev libboost-test-dev libboost-serialization-dev libboost-thread-dev libboost-system-dev"
+
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+git clone https://github.com/EttusResearch/uhd.git
+mkdir -p uhd/host/build
+cd uhd/host/build
+git checkout v4.1.0.4
+# see https://github.com/EttusResearch/uhd/issues/350
+case `uname -m` in
+ arm*)
+ cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_UTILS=OFF -DENABLE_PYTHON_API=OFF -DENABLE_EXAMPLES=OFF -DENABLE_TESTS=OFF -DENABLE_OCTOCLOCK=OFF -DENABLE_MAN_PAGES=OFF -DSTRIP_BINARIES=ON \
+ -DCMAKE_CXX_FLAGS:STRING="-march=armv7-a -mfloat-abi=hard -mfpu=neon -mtune=cortex-a8 -Wno-psabi" \
+ -DCMAKE_C_FLAGS:STRING="-march=armv7-a -mfloat-abi=hard -mfpu=neon -mtune=cortex-a8 -Wno-psabi" \
+ -DCMAKE_ASM_FLAGS:STRING="-march=armv7-a -mfloat-abi=hard -mfpu=neon -mtune=cortex-a8 -g" ..
+ ;;
+ aarch64*)
+ cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_UTILS=OFF -DENABLE_PYTHON_API=OFF -DENABLE_EXAMPLES=OFF -DENABLE_TESTS=OFF -DENABLE_OCTOCLOCK=OFF -DENABLE_MAN_PAGES=OFF -DSTRIP_BINARIES=ON \
+ -DCMAKE_CXX_FLAGS:STRING="-march=armv8-a -mtune=cortex-a72 -Wno-psabi" \
+ -DCMAKE_C_FLAGS:STRING="-march=armv8-a -mtune=cortex-a72 -Wno-psabi" \
+ -DCMAKE_ASM_FLAGS:STRING="-march=armv8-a -mtune=cortex-a72 -g" ..
+ ;;
+ x86_64)
+ cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_UTILS=OFF -DENABLE_PYTHON_API=OFF -DENABLE_EXAMPLES=OFF -DENABLE_TESTS=OFF -DENABLE_OCTOCLOCK=OFF -DENABLE_MAN_PAGES=OFF -DSTRIP_BINARIES=ON ..
+ ;;
+esac
+make
+make install
+cd ../../..
+rm -rf uhd
+
+git clone https://github.com/pothosware/SoapyUHD.git
+cmakebuild SoapyUHD soapy-uhd-0.4.1
+
+SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh
new file mode 100755
index 000000000..c10f97f1f
--- /dev/null
+++ b/docker/scripts/install-dependencies.sh
@@ -0,0 +1,159 @@
+#!/bin/bash
+set -euxo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ if [[ -f ".gitmodules" ]]; then
+ git submodule update --init
+ fi
+ mkdir build
+ cd build
+ cmake ${CMAKE_ARGS:-} ..
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES="libfftw3-single3 libfftw3-double3 python3 python3-setuptools python3-paho-mqtt netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline8 libgfortran5 libgomp1 libasound2 libudev1 ca-certificates libpulse0 libfaad2 libopus0 libboost-program-options1.74.0 libboost-log1.74.0 libcurl4 libncurses6 libliquid1 libconfig++9v5"
+BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev libudev-dev libhamlib-dev patch xsltproc qt5-qmake libfaad-dev libopus-dev libboost-dev libboost-program-options-dev libboost-log-dev libboost-regex-dev libpulse-dev libcurl4-openssl-dev libncurses-dev xz-utils libliquid-dev libconfig++-dev autoconf automake"
+apt-get update
+apt-get -y install auto-apt-proxy
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+case `uname -m` in
+ arm*)
+ PLATFORM=armhf
+ ;;
+ aarch64*)
+ PLATFORM=aarch64
+ ;;
+ x86_64*)
+ PLATFORM=x86_64
+ ;;
+esac
+
+wget https://github.com/just-containers/s6-overlay/releases/download/v3.1.5.0/s6-overlay-noarch.tar.xz
+tar -Jxpf /tmp/s6-overlay-noarch.tar.xz -C /
+rm s6-overlay-noarch.tar.xz
+wget https://github.com/just-containers/s6-overlay/releases/download/v3.1.5.0/s6-overlay-${PLATFORM}.tar.xz
+tar -Jxpf /tmp/s6-overlay-${PLATFORM}.tar.xz -C /
+rm s6-overlay-${PLATFORM}.tar.xz
+
+JS8CALL_VERSION=2.2.0
+JS8CALL_DIR=js8call
+JS8CALL_TGZ=js8call-${JS8CALL_VERSION}.tgz
+wget http://files.js8call.com/${JS8CALL_VERSION}/${JS8CALL_TGZ}
+tar xfz ${JS8CALL_TGZ}
+# patch allows us to build against the packaged hamlib
+patch -Np1 -d ${JS8CALL_DIR} < /js8call-hamlib.patch
+rm /js8call-hamlib.patch
+cmakebuild ${JS8CALL_DIR}
+rm ${JS8CALL_TGZ}
+
+WSJT_DIR=wsjtx-2.6.1
+WSJT_TGZ=${WSJT_DIR}.tgz
+wget https://downloads.sourceforge.net/project/wsjt/${WSJT_DIR}/${WSJT_TGZ}
+tar xfz ${WSJT_TGZ}
+patch -Np0 -d ${WSJT_DIR} < /wsjtx-hamlib.patch
+mv /wsjtx.patch ${WSJT_DIR}
+cmakebuild ${WSJT_DIR}
+rm ${WSJT_TGZ}
+
+git clone https://github.com/alexander-sholohov/msk144decoder.git
+# latest from main as of 2023-02-21
+MAKEFLAGS="" cmakebuild msk144decoder fe2991681e455636e258e83c29fd4b2a72d16095
+
+git clone --depth 1 -b 1.6 https://github.com/wb2osz/direwolf.git
+cd direwolf
+# hamlib is present (necessary for the wsjt-x and js8call builds) and would be used, but there's no real need.
+# this patch prevents direwolf from linking to it, and it can be stripped at the end of the script.
+patch -Np1 < /direwolf-hamlib.patch
+mkdir build
+cd build
+cmake ..
+make
+make install
+cd ../..
+rm -rf direwolf
+# strip lots of generic documentation that will never be read inside a docker container
+rm /usr/local/share/doc/direwolf/*.pdf
+# examples are pointless, too
+rm -rf /usr/local/share/doc/direwolf/examples/
+
+git clone https://github.com/drowe67/codec2.git
+cd codec2
+git checkout 1.2.0
+mkdir build
+cd build
+cmake ..
+make
+make install
+install -m 0755 src/freedv_rx /usr/local/bin
+cd ../..
+rm -rf codec2
+
+wget https://downloads.sourceforge.net/project/drm/dream/2.1.1/dream-2.1.1-svn808.tar.gz
+tar xvfz dream-2.1.1-svn808.tar.gz
+pushd dream
+patch -Np0 < /dream.patch
+qmake CONFIG+=console
+make
+make install
+popd
+rm -rf dream
+rm dream-2.1.1-svn808.tar.gz
+
+git clone https://github.com/mobilinkd/m17-cxx-demod.git
+cmakebuild m17-cxx-demod v2.3
+
+git clone --depth 1 -b v9.0 https://github.com/flightaware/dump1090
+cd dump1090
+make
+install -m 0755 dump1090 /usr/local/bin
+cd ..
+rm -rf dump1090
+
+git clone https://github.com/merbanan/rtl_433.git
+# latest from master as of 2023-09-06
+CMAKE_ARGS="-DENABLE_RTLSDR=OFF" cmakebuild rtl_433 70d84d01e1be87b459f7a10825966f3262b7dd34
+
+git clone https://github.com/szpajder/libacars.git
+cmakebuild libacars v2.2.0
+
+git clone https://github.com/szpajder/dumphfdl
+cmakebuild dumphfdl v1.4.1
+
+git clone https://github.com/szpajder/dumpvdl2.git
+cmakebuild dumpvdl2 v2.3.0
+
+git clone https://github.com/windytan/redsea.git
+pushd redsea
+# latest from master as of 2024-01-18
+git checkout c6e6b47ac2c7a9aac9409483b00ca61cd6eb47bd
+./autogen.sh
+./configure
+make
+make install
+popd
+rm -rf redsea
+
+git clone https://github.com/Opendigitalradio/dablin.git
+CMAKE_ARGS="-DDISABLE_SDL=1" cmakebuild dablin 1.15.0
+
+git clone https://github.com/hessu/aprs-symbols /usr/share/aprs-symbols
+pushd /usr/share/aprs-symbols
+git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802
+# remove unused files (including git meta information)
+rm -rf .git aprs-symbols.ai aprs-sym-export.js
+popd
+
+apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/install-owrx-tools.sh b/docker/scripts/install-owrx-tools.sh
new file mode 100755
index 000000000..357e36b20
--- /dev/null
+++ b/docker/scripts/install-owrx-tools.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+set -euxo pipefail
+export MAKEFLAGS="-j4"
+
+function cmakebuild() {
+ cd $1
+ if [[ ! -z "${2:-}" ]]; then
+ git checkout $2
+ fi
+ mkdir build
+ cd build
+ cmake ${CMAKE_ARGS:-} ..
+ make
+ make install
+ cd ../..
+ rm -rf $1
+}
+
+cd /tmp
+
+STATIC_PACKAGES="libfftw3-single3 libprotobuf32 libsamplerate0 libicu72 libudev1"
+BUILD_PACKAGES="git autoconf automake libtool libfftw3-dev pkg-config cmake make gcc g++ libprotobuf-dev protobuf-compiler libsamplerate-dev libicu-dev libpython3-dev libudev-dev"
+apt-get update
+apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
+
+git clone https://github.com/jketterl/js8py.git
+pushd js8py
+# latest develop as of 2022-11-30 (structured callsign data)
+git checkout f7e394b7892d26cbdcce5d43c0b4081a2a6a48f6
+python3 setup.py install
+popd
+rm -rf js8py
+
+git clone https://github.com/jketterl/csdr.git
+# latest develop as of 2024-01-25 (exemodule setargs)
+cmakebuild csdr 344179a616cdbadf501479ce9ed1b836543e657b
+
+git clone https://github.com/jketterl/pycsdr.git
+cd pycsdr
+# latest develop as of 2024-01-25 (execmodule setargs)
+git checkout 9063b8a119e366c31d089596641a24a427e3cbdc
+./setup.py install install_headers
+cd ..
+rm -rf pycsdr
+
+git clone https://github.com/jketterl/csdr-eti.git
+# latest develop as of 2024-02-13 (fix for aarch64)
+cmakebuild csdr-eti e174007f9c247047dba60f092f794800297c594f
+
+git clone https://github.com/jketterl/pycsdr-eti.git
+cd pycsdr-eti
+# latest develop as of 2024-02-12 (service id filter)
+git checkout 676663b4d796fbadd18dfcae0c3b80eb1b1f9147
+./setup.py install
+cd ..
+rm -rf pycsdr-eti
+
+git clone https://github.com/jketterl/codecserver.git
+mkdir -p /usr/local/etc/codecserver
+cp codecserver/conf/codecserver.conf /usr/local/etc/codecserver
+# latest develop as of 2023-07-03 (error handling)
+cmakebuild codecserver 0f3703ce285acd85fcd28f6620d7795dc173cb50
+
+git clone https://github.com/jketterl/digiham.git
+# latest develop as of 2023-07-02 (codecserver protocol version)
+cmakebuild digiham 262e6dfd9a2c56778bd4b597240756ad0fb9861d
+
+git clone https://github.com/jketterl/pydigiham.git
+cd pydigiham
+# latest develop as of 2023-06-30 (csdr cleanup)
+git checkout 894aa87ea9a3534d1e7109da86194c7cd5e0b7c7
+./setup.py install
+cd ..
+rm -rf pydigiham
+
+apt-get -y purge --autoremove $BUILD_PACKAGES
+apt-get clean
+rm -rf /var/lib/apt/lists/*
diff --git a/docker/scripts/run.sh b/docker/scripts/run.sh
new file mode 100755
index 000000000..92ea968c3
--- /dev/null
+++ b/docker/scripts/run.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+set -euo pipefail
+
+mkdir -p /etc/openwebrx/openwebrx.conf.d
+mkdir -p /var/lib/openwebrx
+mkdir -p /tmp/openwebrx/
+if [[ ! -f /etc/openwebrx/openwebrx.conf.d/20-temporary-directory.conf ]] ; then
+ cat << EOF > /etc/openwebrx/openwebrx.conf.d/20-temporary-directory.conf
+[core]
+temporary_directory = /tmp/openwebrx
+EOF
+fi
+if [[ ! -f /etc/openwebrx/bands.json ]] ; then
+ cp bands.json /etc/openwebrx/
+fi
+if [[ ! -f /etc/openwebrx/openwebrx.conf ]] ; then
+ cp openwebrx.conf /etc/openwebrx/
+fi
+if [[ ! -z "${OPENWEBRX_ADMIN_USER:-}" ]] && [[ ! -z "${OPENWEBRX_ADMIN_PASSWORD:-}" ]] ; then
+ if ! python3 openwebrx.py admin --silent hasuser "${OPENWEBRX_ADMIN_USER}" ; then
+ OWRX_PASSWORD="${OPENWEBRX_ADMIN_PASSWORD}" python3 openwebrx.py admin --noninteractive adduser "${OPENWEBRX_ADMIN_USER}"
+ fi
+fi
+
+
+_term() {
+ echo "Caught signal!"
+ kill -TERM "$child" 2>/dev/null
+}
+
+trap _term SIGTERM SIGINT
+
+python3 openwebrx.py $@ &
+
+child=$!
+wait "$child"
diff --git a/htdocs/__init__.py b/htdocs/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/htdocs/apple-touch-icon.png b/htdocs/apple-touch-icon.png
new file mode 100644
index 000000000..5bc3c155a
Binary files /dev/null and b/htdocs/apple-touch-icon.png differ
diff --git a/htdocs/css/admin.css b/htdocs/css/admin.css
new file mode 100644
index 000000000..02e9a8adb
--- /dev/null
+++ b/htdocs/css/admin.css
@@ -0,0 +1,166 @@
+@import url("openwebrx-header.css");
+@import url("openwebrx-globals.css");
+
+html, body {
+ height: unset;
+}
+
+body {
+ margin-bottom: 5rem;
+}
+
+hr {
+ background: #444;
+}
+
+.buttons {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background-color: #222;
+ z-index: 2;
+ padding: 10px;
+ text-align: right;
+ border-top: 1px solid #444;
+}
+
+.row .map-input {
+ margin: 15px 15px 0;
+}
+
+.settings-section h3 {
+ margin-top: 1em;
+ margin-bottom: 1em;
+}
+
+h1 {
+ margin: 1em 0;
+ text-align: center;
+}
+
+.matrix {
+ display: grid;
+}
+
+.q65-matrix {
+ grid-template-columns: repeat(5, auto);
+}
+
+.imageupload .image-container {
+ max-width: 100%;
+ padding: 7px;
+}
+
+.imageupload img.webrx-top-photo {
+ max-height: 350px;
+ max-width: 100%;
+}
+
+.settings-grid > div {
+ padding: 20px;
+}
+
+.settings-grid .btn {
+ width: 100%;
+ height: 100px;
+ padding: 20px;
+ font-size: 1.2rem;
+}
+
+.tab-body {
+ overflow: auto;
+ border: 1px solid #444;
+ border-top: none;
+ border-bottom-left-radius: 0.25rem;
+ border-bottom-right-radius: 0.25rem;
+}
+
+.tab-body .form-group {
+ padding-right: 15px;
+}
+
+.bookmarks table .frequency, .bookmark-list table .frequency {
+ text-align: right;
+}
+
+.bookmarks table input, .bookmarks table select {
+ width: initial;
+ text-align: inherit;
+ display: initial;
+}
+
+.bookmark-list table .form-check-input {
+ margin-left: 0;
+}
+
+.actions {
+ margin: 1rem 0;
+}
+
+.actions .btn {
+ width: 100%;
+}
+
+.wsjt-decoding-depths-table {
+ width: auto;
+ margin: 0;
+}
+
+.wsjt-decoding-depths-table td:first-child {
+ padding-left: 0;
+}
+
+.sdr-device-list .list-group-item,
+.sdr-profile-list .list-group-item {
+ background: initial;
+}
+
+.sdr-device-list .sdr-profile-list {
+ max-height: 20rem;
+ overflow-y: auto;
+}
+
+.removable-group.removable, .add-group {
+ display: flex;
+ flex-direction: row;
+}
+
+.removable-group.removable .removable-item, .add-group .add-group-select {
+ flex: 1 0 0;
+ margin-right: .25rem;
+}
+
+.removable-group.removable .option-remove-button, .add-group .option-add-button {
+ flex: 0 0 70px;
+}
+
+.option-add-button, .option-remove-button {
+ width: 70px;
+}
+
+.scheduler-static-time-inputs {
+ display: flex;
+ flex-direction: row;
+}
+
+.scheduler-static-time-inputs > * {
+ flex: 0 0 auto;
+ width: unset;
+}
+
+.scheduler-static-time-inputs > select {
+ flex: 1 0 auto;
+}
+
+.breadcrumb {
+ margin-top: .5rem;
+}
+
+.imageupload.is-invalid ~ .invalid-feedback {
+ display: block;
+}
+
+.device-log-messages {
+ max-height: 500px;
+}
\ No newline at end of file
diff --git a/htdocs/css/bootstrap.min.css b/htdocs/css/bootstrap.min.css
new file mode 100644
index 000000000..43d80a04d
--- /dev/null
+++ b/htdocs/css/bootstrap.min.css
@@ -0,0 +1,12 @@
+/*!
+ * Bootswatch v4.5.0
+ * Homepage: https://bootswatch.com
+ * Copyright 2012-2020 Thomas Park
+ * Licensed under MIT
+ * Based on Bootstrap
+*//*!
+ * Bootstrap v4.5.0 (https://getbootstrap.com/)
+ * Copyright 2011-2020 The Bootstrap Authors
+ * Copyright 2011-2020 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */@import url("https://fonts.googleapis.com/css?family=Lato:400,700,400italic&display=swap");:root{--blue: #375a7f;--indigo: #6610f2;--purple: #6f42c1;--pink: #e83e8c;--red: #E74C3C;--orange: #fd7e14;--yellow: #F39C12;--green: #00bc8c;--teal: #20c997;--cyan: #3498DB;--white: #fff;--gray: #888;--gray-dark: #303030;--primary: #375a7f;--secondary: #444;--success: #00bc8c;--info: #3498DB;--warning: #F39C12;--danger: #E74C3C;--light: #adb5bd;--dark: #303030;--breakpoint-xs: 0;--breakpoint-sm: 576px;--breakpoint-md: 768px;--breakpoint-lg: 992px;--breakpoint-xl: 1200px;--font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}*,*::before,*::after{-webkit-box-sizing:border-box;box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:"Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-size:0.9375rem;font-weight:400;line-height:1.5;color:#fff;text-align:left;background-color:#222}[tabindex="-1"]:focus:not(:focus-visible){outline:0 !important}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:0.5rem}p{margin-top:0;margin-bottom:1rem}abbr[title],abbr[data-original-title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#00bc8c;text-decoration:none;background-color:transparent}a:hover{color:#007053;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:0.75rem;padding-bottom:0.75rem;color:#888;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:0.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role="button"]{cursor:pointer}select{word-wrap:normal}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button:not(:disabled),[type="button"]:not(:disabled),[type="reset"]:not(:disabled),[type="submit"]:not(:disabled){cursor:pointer}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{padding:0;border-style:none}input[type="radio"],input[type="checkbox"]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{outline-offset:-2px;-webkit-appearance:none}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none !important}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{margin-bottom:0.5rem;font-weight:500;line-height:1.2}h1,.h1{font-size:3rem}h2,.h2{font-size:2.5rem}h3,.h3{font-size:2rem}h4,.h4{font-size:1.40625rem}h5,.h5{font-size:1.171875rem}h6,.h6{font-size:0.9375rem}.lead{font-size:1.171875rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,0.1)}small,.small{font-size:80%;font-weight:400}mark,.mark{padding:0.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:0.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.171875rem}.blockquote-footer{display:block;font-size:80%;color:#888}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:0.25rem;background-color:#222;border:1px solid #dee2e6;border-radius:0.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:0.5rem;line-height:1}.figure-caption{font-size:90%;color:#888}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:0.2rem 0.4rem;font-size:87.5%;color:#fff;background-color:#222;border-radius:0.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:inherit}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width: 576px){.container{max-width:540px}}@media (min-width: 768px){.container{max-width:720px}}@media (min-width: 992px){.container{max-width:960px}}@media (min-width: 1200px){.container{max-width:1140px}}.container-fluid,.container-sm,.container-md,.container-lg,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width: 576px){.container,.container-sm{max-width:540px}}@media (min-width: 768px){.container,.container-sm,.container-md{max-width:720px}}@media (min-width: 992px){.container,.container-sm,.container-md,.container-lg{max-width:960px}}@media (min-width: 1200px){.container,.container-sm,.container-md,.container-lg,.container-xl{max-width:1140px}}.row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*="col-"]{padding-right:0;padding-left:0}.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12,.col,.col-auto,.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,.col-sm,.col-sm-auto,.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,.col-md,.col-md-auto,.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,.col-lg,.col-lg-auto,.col-xl-1,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-1{margin-left:8.3333333333%}.offset-2{margin-left:16.6666666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.3333333333%}.offset-5{margin-left:41.6666666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.3333333333%}.offset-8{margin-left:66.6666666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.3333333333%}.offset-11{margin-left:91.6666666667%}@media (min-width: 576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-sm-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-sm-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-sm-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-sm-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-sm-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-sm-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-sm-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-sm-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-sm-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-sm-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-sm-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-sm-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-sm-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-sm-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-sm-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-sm-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-sm-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-sm-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-sm-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-sm-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-sm-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-sm-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-sm-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-sm-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-sm-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.3333333333%}.offset-sm-2{margin-left:16.6666666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.3333333333%}.offset-sm-5{margin-left:41.6666666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.3333333333%}.offset-sm-8{margin-left:66.6666666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.3333333333%}.offset-sm-11{margin-left:91.6666666667%}}@media (min-width: 768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-md-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-md-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-md-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-md-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-md-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-md-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-md-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-md-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-md-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-md-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-md-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-md-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-md-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-md-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-md-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-md-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-md-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-md-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-md-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-md-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-md-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-md-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-md-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-md-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-md-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.3333333333%}.offset-md-2{margin-left:16.6666666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.3333333333%}.offset-md-5{margin-left:41.6666666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.3333333333%}.offset-md-8{margin-left:66.6666666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.3333333333%}.offset-md-11{margin-left:91.6666666667%}}@media (min-width: 992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-lg-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-lg-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-lg-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-lg-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-lg-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-lg-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-lg-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-lg-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-lg-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-lg-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-lg-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-lg-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-lg-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-lg-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-lg-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-lg-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-lg-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-lg-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-lg-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-lg-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-lg-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-lg-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-lg-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-lg-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-lg-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.3333333333%}.offset-lg-2{margin-left:16.6666666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.3333333333%}.offset-lg-5{margin-left:41.6666666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.3333333333%}.offset-lg-8{margin-left:66.6666666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.3333333333%}.offset-lg-11{margin-left:91.6666666667%}}@media (min-width: 1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-xl-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-xl-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-xl-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-xl-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-xl-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-xl-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-xl-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-xl-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-xl-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-xl-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-xl-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-xl-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-xl-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-xl-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-xl-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-xl-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-xl-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-xl-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-xl-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-xl-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-xl-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-xl-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-xl-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-xl-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-xl-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.3333333333%}.offset-xl-2{margin-left:16.6666666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.3333333333%}.offset-xl-5{margin-left:41.6666666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.3333333333%}.offset-xl-8{margin-left:66.6666666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.3333333333%}.offset-xl-11{margin-left:91.6666666667%}}.table{width:100%;margin-bottom:1rem;color:#fff}.table th,.table td{padding:0.75rem;vertical-align:top;border-top:1px solid #444}.table thead th{vertical-align:bottom;border-bottom:2px solid #444}.table tbody+tbody{border-top:2px solid #444}.table-sm th,.table-sm td{padding:0.3rem}.table-bordered{border:1px solid #444}.table-bordered th,.table-bordered td{border:1px solid #444}.table-bordered thead th,.table-bordered thead td{border-bottom-width:2px}.table-borderless th,.table-borderless td,.table-borderless thead th,.table-borderless tbody+tbody{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:#303030}.table-hover tbody tr:hover{color:#fff;background-color:rgba(0,0,0,0.075)}.table-primary,.table-primary>th,.table-primary>td{background-color:#c7d1db}.table-primary th,.table-primary td,.table-primary thead th,.table-primary tbody+tbody{border-color:#97a9bc}.table-hover .table-primary:hover{background-color:#b7c4d1}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#b7c4d1}.table-secondary,.table-secondary>th,.table-secondary>td{background-color:#cbcbcb}.table-secondary th,.table-secondary td,.table-secondary thead th,.table-secondary tbody+tbody{border-color:#9e9e9e}.table-hover .table-secondary:hover{background-color:#bebebe}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#bebebe}.table-success,.table-success>th,.table-success>td{background-color:#b8ecdf}.table-success th,.table-success td,.table-success thead th,.table-success tbody+tbody{border-color:#7adcc3}.table-hover .table-success:hover{background-color:#a4e7d6}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#a4e7d6}.table-info,.table-info>th,.table-info>td{background-color:#c6e2f5}.table-info th,.table-info td,.table-info thead th,.table-info tbody+tbody{border-color:#95c9ec}.table-hover .table-info:hover{background-color:#b0d7f1}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#b0d7f1}.table-warning,.table-warning>th,.table-warning>td{background-color:#fce3bd}.table-warning th,.table-warning td,.table-warning thead th,.table-warning tbody+tbody{border-color:#f9cc84}.table-hover .table-warning:hover{background-color:#fbd9a5}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#fbd9a5}.table-danger,.table-danger>th,.table-danger>td{background-color:#f8cdc8}.table-danger th,.table-danger td,.table-danger thead th,.table-danger tbody+tbody{border-color:#f3a29a}.table-hover .table-danger:hover{background-color:#f5b8b1}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f5b8b1}.table-light,.table-light>th,.table-light>td{background-color:#e8eaed}.table-light th,.table-light td,.table-light thead th,.table-light tbody+tbody{border-color:#d4d9dd}.table-hover .table-light:hover{background-color:#dadde2}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#dadde2}.table-dark,.table-dark>th,.table-dark>td{background-color:#c5c5c5}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#939393}.table-hover .table-dark:hover{background-color:#b8b8b8}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b8b8b8}.table-active,.table-active>th,.table-active>td{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,0.075)}.table .thead-dark th{color:#fff;background-color:#303030;border-color:#434343}.table .thead-light th{color:#444;background-color:#ebebeb;border-color:#444}.table-dark{color:#fff;background-color:#303030}.table-dark th,.table-dark td,.table-dark thead th{border-color:#434343}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,0.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,0.075)}@media (max-width: 575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width: 767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width: 991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width: 1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 0.75rem;font-size:0.9375rem;font-weight:400;line-height:1.5;color:#444;background-color:#fff;background-clip:padding-box;border:1px solid #222;border-radius:0.25rem;-webkit-transition:border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control{-webkit-transition:none;transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #444}.form-control:focus{color:#444;background-color:#fff;border-color:#739ac2;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.form-control::-webkit-input-placeholder{color:#888;opacity:1}.form-control::-ms-input-placeholder{color:#888;opacity:1}.form-control::placeholder{color:#888;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#ebebeb;opacity:1}input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:focus::-ms-value{color:#444;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(0.375rem + 1px);padding-bottom:calc(0.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(0.5rem + 1px);padding-bottom:calc(0.5rem + 1px);font-size:1.171875rem;line-height:1.5}.col-form-label-sm{padding-top:calc(0.25rem + 1px);padding-bottom:calc(0.25rem + 1px);font-size:0.8203125rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:0.375rem 0;margin-bottom:0;font-size:0.9375rem;line-height:1.5;color:#fff;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + 0.5rem + 2px);padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5;border-radius:0.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:0.5rem 1rem;font-size:1.171875rem;line-height:1.5;border-radius:0.3rem}select.form-control[size],select.form-control[multiple]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:0.25rem}.form-row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*="col-"]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:0.3rem;margin-left:-1.25rem}.form-check-input[disabled] ~ .form-check-label,.form-check-input:disabled ~ .form-check-label{color:#888}.form-check-label{margin-bottom:0}.form-check-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:0.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:0.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:0.25rem;font-size:80%;color:#00bc8c}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:0.25rem 0.5rem;margin-top:.1rem;font-size:0.8203125rem;line-height:1.5;color:#fff;background-color:rgba(0,188,140,0.9);border-radius:0.25rem}.was-validated :valid ~ .valid-feedback,.was-validated :valid ~ .valid-tooltip,.is-valid ~ .valid-feedback,.is-valid ~ .valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-valid{border-color:#00bc8c;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:#00bc8c;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .custom-select:valid,.custom-select.is-valid{border-color:#00bc8c;padding-right:calc(0.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .custom-select:valid:focus,.custom-select.is-valid:focus{border-color:#00bc8c;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.was-validated .form-check-input:valid ~ .form-check-label,.form-check-input.is-valid ~ .form-check-label{color:#00bc8c}.was-validated .form-check-input:valid ~ .valid-feedback,.was-validated .form-check-input:valid ~ .valid-tooltip,.form-check-input.is-valid ~ .valid-feedback,.form-check-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-control-input:valid ~ .custom-control-label,.custom-control-input.is-valid ~ .custom-control-label{color:#00bc8c}.was-validated .custom-control-input:valid ~ .custom-control-label::before,.custom-control-input.is-valid ~ .custom-control-label::before{border-color:#00bc8c}.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before,.custom-control-input.is-valid:checked ~ .custom-control-label::before{border-color:#00efb2;background-color:#00efb2}.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before,.custom-control-input.is-valid:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before{border-color:#00bc8c}.was-validated .custom-file-input:valid ~ .custom-file-label,.custom-file-input.is-valid ~ .custom-file-label{border-color:#00bc8c}.was-validated .custom-file-input:valid:focus ~ .custom-file-label,.custom-file-input.is-valid:focus ~ .custom-file-label{border-color:#00bc8c;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.invalid-feedback{display:none;width:100%;margin-top:0.25rem;font-size:80%;color:#E74C3C}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:0.25rem 0.5rem;margin-top:.1rem;font-size:0.8203125rem;line-height:1.5;color:#fff;background-color:rgba(231,76,60,0.9);border-radius:0.25rem}.was-validated :invalid ~ .invalid-feedback,.was-validated :invalid ~ .invalid-tooltip,.is-invalid ~ .invalid-feedback,.is-invalid ~ .invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:#E74C3C;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23E74C3C' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23E74C3C' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:#E74C3C;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .custom-select:invalid,.custom-select.is-invalid{border-color:#E74C3C;padding-right:calc(0.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23E74C3C' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23E74C3C' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .custom-select:invalid:focus,.custom-select.is-invalid:focus{border-color:#E74C3C;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.was-validated .form-check-input:invalid ~ .form-check-label,.form-check-input.is-invalid ~ .form-check-label{color:#E74C3C}.was-validated .form-check-input:invalid ~ .invalid-feedback,.was-validated .form-check-input:invalid ~ .invalid-tooltip,.form-check-input.is-invalid ~ .invalid-feedback,.form-check-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-control-input:invalid ~ .custom-control-label,.custom-control-input.is-invalid ~ .custom-control-label{color:#E74C3C}.was-validated .custom-control-input:invalid ~ .custom-control-label::before,.custom-control-input.is-invalid ~ .custom-control-label::before{border-color:#E74C3C}.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before,.custom-control-input.is-invalid:checked ~ .custom-control-label::before{border-color:#ed7669;background-color:#ed7669}.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before,.custom-control-input.is-invalid:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before{border-color:#E74C3C}.was-validated .custom-file-input:invalid ~ .custom-file-label,.custom-file-input.is-invalid ~ .custom-file-label{border-color:#E74C3C}.was-validated .custom-file-input:invalid:focus ~ .custom-file-label,.custom-file-input.is-invalid:focus ~ .custom-file-label{border-color:#E74C3C;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.form-inline{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width: 576px){.form-inline label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .input-group,.form-inline .custom-select{width:auto}.form-inline .form-check{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:0.25rem;margin-left:0}.form-inline .custom-control{-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#fff;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:0.375rem 0.75rem;font-size:0.9375rem;line-height:1.5;border-radius:0.25rem;-webkit-transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.btn{-webkit-transition:none;transition:none}}.btn:hover{color:#fff;text-decoration:none}.btn:focus,.btn.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.btn.disabled,.btn:disabled{opacity:0.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-primary:hover{color:#fff;background-color:#2b4764;border-color:#28415b}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#2b4764;border-color:#28415b;-webkit-box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5);box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-primary:not(:disabled):not(.disabled):active,.btn-primary:not(:disabled):not(.disabled).active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#28415b;border-color:#243a53}.btn-primary:not(:disabled):not(.disabled):active:focus,.btn-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-primary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5);box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5)}.btn-secondary{color:#fff;background-color:#444;border-color:#444}.btn-secondary:hover{color:#fff;background-color:#313131;border-color:#2b2a2a}.btn-secondary:focus,.btn-secondary.focus{color:#fff;background-color:#313131;border-color:#2b2a2a;-webkit-box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5);box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#444;border-color:#444}.btn-secondary:not(:disabled):not(.disabled):active,.btn-secondary:not(:disabled):not(.disabled).active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#2b2a2a;border-color:#242424}.btn-secondary:not(:disabled):not(.disabled):active:focus,.btn-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-secondary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5);box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5)}.btn-success{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-success:hover{color:#fff;background-color:#009670;border-color:#008966}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#009670;border-color:#008966;-webkit-box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5);box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-success:not(:disabled):not(.disabled):active,.btn-success:not(:disabled):not(.disabled).active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#008966;border-color:#007c5d}.btn-success:not(:disabled):not(.disabled):active:focus,.btn-success:not(:disabled):not(.disabled).active:focus,.show>.btn-success.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5);box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5)}.btn-info{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-info:hover{color:#fff;background-color:#2384c6;border-color:#217dbb}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#2384c6;border-color:#217dbb;-webkit-box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5);box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-info:not(:disabled):not(.disabled):active,.btn-info:not(:disabled):not(.disabled).active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#217dbb;border-color:#1f76b0}.btn-info:not(:disabled):not(.disabled):active:focus,.btn-info:not(:disabled):not(.disabled).active:focus,.show>.btn-info.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5);box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5)}.btn-warning{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-warning:hover{color:#fff;background-color:#d4860b;border-color:#c87f0a}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#d4860b;border-color:#c87f0a;-webkit-box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5);box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5)}.btn-warning.disabled,.btn-warning:disabled{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-warning:not(:disabled):not(.disabled):active,.btn-warning:not(:disabled):not(.disabled).active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#c87f0a;border-color:#bc770a}.btn-warning:not(:disabled):not(.disabled):active:focus,.btn-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-warning.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5);box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5)}.btn-danger{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-danger:hover{color:#fff;background-color:#e12e1c;border-color:#d62c1a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#e12e1c;border-color:#d62c1a;-webkit-box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5);box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-danger:not(:disabled):not(.disabled):active,.btn-danger:not(:disabled):not(.disabled).active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#d62c1a;border-color:#ca2a19}.btn-danger:not(:disabled):not(.disabled):active:focus,.btn-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-danger.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5);box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5)}.btn-light{color:#222;background-color:#adb5bd;border-color:#adb5bd}.btn-light:hover{color:#fff;background-color:#98a2ac;border-color:#919ca6}.btn-light:focus,.btn-light.focus{color:#fff;background-color:#98a2ac;border-color:#919ca6;-webkit-box-shadow:0 0 0 0.2rem rgba(152,159,166,0.5);box-shadow:0 0 0 0.2rem rgba(152,159,166,0.5)}.btn-light.disabled,.btn-light:disabled{color:#222;background-color:#adb5bd;border-color:#adb5bd}.btn-light:not(:disabled):not(.disabled):active,.btn-light:not(:disabled):not(.disabled).active,.show>.btn-light.dropdown-toggle{color:#fff;background-color:#919ca6;border-color:#8a95a1}.btn-light:not(:disabled):not(.disabled):active:focus,.btn-light:not(:disabled):not(.disabled).active:focus,.show>.btn-light.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(152,159,166,0.5);box-shadow:0 0 0 0.2rem rgba(152,159,166,0.5)}.btn-dark{color:#fff;background-color:#303030;border-color:#303030}.btn-dark:hover{color:#fff;background-color:#1d1d1d;border-color:#171616}.btn-dark:focus,.btn-dark.focus{color:#fff;background-color:#1d1d1d;border-color:#171616;-webkit-box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5);box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#303030;border-color:#303030}.btn-dark:not(:disabled):not(.disabled):active,.btn-dark:not(:disabled):not(.disabled).active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#171616;border-color:#101010}.btn-dark:not(:disabled):not(.disabled):active:focus,.btn-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-dark.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5);box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5)}.btn-outline-primary{color:#375a7f;border-color:#375a7f}.btn-outline-primary:hover{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-outline-primary:focus,.btn-outline-primary.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#375a7f;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled):active,.btn-outline-primary:not(:disabled):not(.disabled).active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5)}.btn-outline-secondary{color:#444;border-color:#444}.btn-outline-secondary:hover{color:#fff;background-color:#444;border-color:#444}.btn-outline-secondary:focus,.btn-outline-secondary.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5);box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#444;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled):active,.btn-outline-secondary:not(:disabled):not(.disabled).active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#444;border-color:#444}.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5);box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5)}.btn-outline-success{color:#00bc8c;border-color:#00bc8c}.btn-outline-success:hover{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-outline-success:focus,.btn-outline-success.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#00bc8c;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled):active,.btn-outline-success:not(:disabled):not(.disabled).active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-outline-success:not(:disabled):not(.disabled):active:focus,.btn-outline-success:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-success.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5)}.btn-outline-info{color:#3498DB;border-color:#3498DB}.btn-outline-info:hover{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-outline-info:focus,.btn-outline-info.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5);box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#3498DB;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled):active,.btn-outline-info:not(:disabled):not(.disabled).active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-outline-info:not(:disabled):not(.disabled):active:focus,.btn-outline-info:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-info.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5);box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5)}.btn-outline-warning{color:#F39C12;border-color:#F39C12}.btn-outline-warning:hover{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-outline-warning:focus,.btn-outline-warning.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5);box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#F39C12;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled):active,.btn-outline-warning:not(:disabled):not(.disabled).active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5);box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5)}.btn-outline-danger{color:#E74C3C;border-color:#E74C3C}.btn-outline-danger:hover{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-outline-danger:focus,.btn-outline-danger.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#E74C3C;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled):active,.btn-outline-danger:not(:disabled):not(.disabled).active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5)}.btn-outline-light{color:#adb5bd;border-color:#adb5bd}.btn-outline-light:hover{color:#222;background-color:#adb5bd;border-color:#adb5bd}.btn-outline-light:focus,.btn-outline-light.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5);box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#adb5bd;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled):active,.btn-outline-light:not(:disabled):not(.disabled).active,.show>.btn-outline-light.dropdown-toggle{color:#222;background-color:#adb5bd;border-color:#adb5bd}.btn-outline-light:not(:disabled):not(.disabled):active:focus,.btn-outline-light:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-light.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5);box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5)}.btn-outline-dark{color:#303030;border-color:#303030}.btn-outline-dark:hover{color:#fff;background-color:#303030;border-color:#303030}.btn-outline-dark:focus,.btn-outline-dark.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5);box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#303030;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled):active,.btn-outline-dark:not(:disabled):not(.disabled).active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#303030;border-color:#303030}.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5);box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5)}.btn-link{font-weight:400;color:#00bc8c;text-decoration:none}.btn-link:hover{color:#007053;text-decoration:underline}.btn-link:focus,.btn-link.focus{text-decoration:underline}.btn-link:disabled,.btn-link.disabled{color:#888;pointer-events:none}.btn-lg,.btn-group-lg>.btn{padding:0.5rem 1rem;font-size:1.171875rem;line-height:1.5;border-radius:0.3rem}.btn-sm,.btn-group-sm>.btn{padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5;border-radius:0.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:0.5rem}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{-webkit-transition:opacity 0.15s linear;transition:opacity 0.15s linear}@media (prefers-reduced-motion: reduce){.fade{-webkit-transition:none;transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height 0.35s ease;transition:height 0.35s ease}@media (prefers-reduced-motion: reduce){.collapsing{-webkit-transition:none;transition:none}}.dropup,.dropright,.dropdown,.dropleft{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid;border-right:0.3em solid transparent;border-bottom:0;border-left:0.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:0.5rem 0;margin:0.125rem 0 0;font-size:0.9375rem;color:#fff;text-align:left;list-style:none;background-color:#222;background-clip:padding-box;border:1px solid #444;border-radius:0.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width: 576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width: 768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width: 992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width: 1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:0.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0;border-right:0.3em solid transparent;border-bottom:0.3em solid;border-left:0.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:0.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid transparent;border-right:0;border-bottom:0.3em solid transparent;border-left:0.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:0.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid transparent;border-right:0.3em solid;border-bottom:0.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^="top"],.dropdown-menu[x-placement^="right"],.dropdown-menu[x-placement^="bottom"],.dropdown-menu[x-placement^="left"]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:0.5rem 0;overflow:hidden;border-top:1px solid #444}.dropdown-item{display:block;width:100%;padding:0.25rem 1.5rem;clear:both;font-weight:400;color:#fff;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:hover,.dropdown-item:focus{color:#fff;text-decoration:none;background-color:#375a7f}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#375a7f}.dropdown-item.disabled,.dropdown-item:disabled{color:#888;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:0.5rem 1.5rem;margin-bottom:0;font-size:0.8203125rem;color:#888;white-space:nowrap}.dropdown-item-text{display:block;padding:0.25rem 1.5rem;color:#fff}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover{z-index:1}.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child){margin-left:-1px}.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:0.5625rem;padding-left:0.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:0.375rem;padding-left:0.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:0.75rem;padding-left:0.75rem}.btn-group-vertical{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type="radio"],.btn-group-toggle>.btn input[type="checkbox"],.btn-group-toggle>.btn-group>.btn input[type="radio"],.btn-group-toggle>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-control-plaintext,.input-group>.custom-select,.input-group>.custom-file{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.form-control+.form-control,.input-group>.form-control+.custom-select,.input-group>.form-control+.custom-file,.input-group>.form-control-plaintext+.form-control,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.custom-file,.input-group>.custom-select+.form-control,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.custom-file,.input-group>.custom-file+.form-control,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.custom-file{margin-left:-1px}.input-group>.form-control:focus,.input-group>.custom-select:focus,.input-group>.custom-file .custom-file-input:focus ~ .custom-file-label{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.form-control:not(:last-child),.input-group>.custom-select:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.form-control:not(:first-child),.input-group>.custom-select:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-prepend,.input-group-append{display:-webkit-box;display:-ms-flexbox;display:flex}.input-group-prepend .btn,.input-group-append .btn{position:relative;z-index:2}.input-group-prepend .btn:focus,.input-group-append .btn:focus{z-index:3}.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.input-group-text,.input-group-append .input-group-text+.btn{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:0.375rem 0.75rem;margin-bottom:0;font-size:0.9375rem;font-weight:400;line-height:1.5;color:#adb5bd;text-align:center;white-space:nowrap;background-color:#444;border:1px solid #222;border-radius:0.25rem}.input-group-text input[type="radio"],.input-group-text input[type="checkbox"]{margin-top:0}.input-group-lg>.form-control:not(textarea),.input-group-lg>.custom-select{height:calc(1.5em + 1rem + 2px)}.input-group-lg>.form-control,.input-group-lg>.custom-select,.input-group-lg>.input-group-prepend>.input-group-text,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-append>.btn{padding:0.5rem 1rem;font-size:1.171875rem;line-height:1.5;border-radius:0.3rem}.input-group-sm>.form-control:not(textarea),.input-group-sm>.custom-select{height:calc(1.5em + 0.5rem + 2px)}.input-group-sm>.form-control,.input-group-sm>.custom-select,.input-group-sm>.input-group-prepend>.input-group-text,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-append>.btn{padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5;border-radius:0.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text,.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.40625rem;padding-left:1.5rem}.custom-control-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.203125rem;opacity:0}.custom-control-input:checked ~ .custom-control-label::before{color:#fff;border-color:#375a7f;background-color:#375a7f}.custom-control-input:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-control-input:focus:not(:checked) ~ .custom-control-label::before{border-color:#739ac2}.custom-control-input:not(:disabled):active ~ .custom-control-label::before{color:#fff;background-color:#97b3d2;border-color:#97b3d2}.custom-control-input[disabled] ~ .custom-control-label,.custom-control-input:disabled ~ .custom-control-label{color:#888}.custom-control-input[disabled] ~ .custom-control-label::before,.custom-control-input:disabled ~ .custom-control-label::before{background-color:#ebebeb}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:0.203125rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:0.203125rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50% / 50% 50%}.custom-checkbox .custom-control-label::before{border-radius:0.25rem}.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before{border-color:#375a7f;background-color:#375a7f}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:0.5rem}.custom-switch .custom-control-label::after{top:calc(0.203125rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:0.5rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-switch .custom-control-label::after{-webkit-transition:none;transition:none}}.custom-switch .custom-control-input:checked ~ .custom-control-label::after{background-color:#fff;-webkit-transform:translateX(0.75rem);transform:translateX(0.75rem)}.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 1.75rem 0.375rem 0.75rem;font-size:0.9375rem;font-weight:400;line-height:1.5;color:#444;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px;border:1px solid #222;border-radius:0.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#739ac2;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-select:focus::-ms-value{color:#444;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:0.75rem;background-image:none}.custom-select:disabled{color:#888;background-color:#ebebeb}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #444}.custom-select-sm{height:calc(1.5em + 0.5rem + 2px);padding-top:0.25rem;padding-bottom:0.25rem;padding-left:0.5rem;font-size:0.8203125rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:0.5rem;padding-bottom:0.5rem;padding-left:1rem;font-size:1.171875rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + 0.75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + 0.75rem + 2px);margin:0;opacity:0}.custom-file-input:focus ~ .custom-file-label{border-color:#739ac2;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-file-input[disabled] ~ .custom-file-label,.custom-file-input:disabled ~ .custom-file-label{background-color:#ebebeb}.custom-file-input:lang(en) ~ .custom-file-label::after{content:"Browse"}.custom-file-input ~ .custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 0.75rem;font-weight:400;line-height:1.5;color:#adb5bd;background-color:#fff;border:1px solid #222;border-radius:0.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + 0.75rem);padding:0.375rem 0.75rem;line-height:1.5;color:#adb5bd;content:"Browse";background-color:#444;border-left:inherit;border-radius:0 0.25rem 0.25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:none}.custom-range:focus::-webkit-slider-thumb{-webkit-box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-0.25rem;background-color:#375a7f;border:0;border-radius:1rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#97b3d2}.custom-range::-webkit-slider-runnable-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#375a7f;border:0;border-radius:1rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-moz-range-thumb{-webkit-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#97b3d2}.custom-range::-moz-range-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:0.2rem;margin-left:0.2rem;background-color:#375a7f;border:0;border-radius:1rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-ms-thumb{-webkit-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#97b3d2}.custom-range::-ms-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:0.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-control-label::before,.custom-file-label,.custom-select{-webkit-transition:none;transition:none}}.nav{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:0.5rem 2rem}.nav-link:hover,.nav-link:focus{text-decoration:none}.nav-link.disabled{color:#adb5bd;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #444}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:0.25rem;border-top-right-radius:0.25rem}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{border-color:#444 #444 transparent}.nav-tabs .nav-link.disabled{color:#adb5bd;background-color:transparent;border-color:transparent}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:#fff;background-color:#222;border-color:#444 #444 transparent}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:0.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#375a7f}.nav-fill .nav-item{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-sm,.navbar .container-md,.navbar .container-lg,.navbar .container-xl{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:0.32421875rem;padding-bottom:0.32421875rem;margin-right:1rem;font-size:1.171875rem;line-height:inherit;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-nav{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:0.5rem;padding-bottom:0.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:0.25rem 0.75rem;font-size:1.171875rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:0.25rem}.navbar-toggler:hover,.navbar-toggler:focus{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width: 575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 576px){.navbar-expand-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width: 767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-md,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 768px){.navbar-expand-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-md,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width: 991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 992px){.navbar-expand-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width: 1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 1200px){.navbar-expand-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-sm,.navbar-expand>.container-md,.navbar-expand>.container-lg,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-sm,.navbar-expand>.container-md,.navbar-expand>.container-lg,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:#222}.navbar-light .navbar-brand:hover,.navbar-light .navbar-brand:focus{color:#222}.navbar-light .navbar-nav .nav-link{color:rgba(34,34,34,0.7)}.navbar-light .navbar-nav .nav-link:hover,.navbar-light .navbar-nav .nav-link:focus{color:#222}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,0.3)}.navbar-light .navbar-nav .show>.nav-link,.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .nav-link.active{color:#222}.navbar-light .navbar-toggler{color:rgba(34,34,34,0.7);border-color:rgba(34,34,34,0.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2834, 34, 34, 0.7%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(34,34,34,0.7)}.navbar-light .navbar-text a{color:#222}.navbar-light .navbar-text a:hover,.navbar-light .navbar-text a:focus{color:#222}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:hover,.navbar-dark .navbar-brand:focus{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,0.6)}.navbar-dark .navbar-nav .nav-link:hover,.navbar-dark .navbar-nav .nav-link:focus{color:#fff}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,0.25)}.navbar-dark .navbar-nav .show>.nav-link,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .nav-link.active{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,0.6);border-color:rgba(255,255,255,0.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.6%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,0.6)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:hover,.navbar-dark .navbar-text a:focus{color:#fff}.card{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#303030;background-clip:border-box;border:1px solid rgba(0,0,0,0.125);border-radius:0.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(0.25rem - 1px);border-bottom-left-radius:calc(0.25rem - 1px)}.card-body{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:0.75rem}.card-subtitle{margin-top:-0.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:0.75rem 1.25rem;margin-bottom:0;background-color:#444;border-bottom:1px solid rgba(0,0,0,0.125)}.card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:0.75rem 1.25rem;background-color:#444;border-top:1px solid rgba(0,0,0,0.125)}.card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.card-header-tabs{margin-right:-0.625rem;margin-bottom:-0.75rem;margin-left:-0.625rem;border-bottom:0}.card-header-pills{margin-right:-0.625rem;margin-left:-0.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img,.card-img-top,.card-img-bottom{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(0.25rem - 1px);border-bottom-left-radius:calc(0.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width: 576px){.card-deck{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width: 576px){.card-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-img-top,.card-group>.card:not(:last-child) .card-header{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-img-bottom,.card-group>.card:not(:last-child) .card-footer{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-img-top,.card-group>.card:not(:first-child) .card-header{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-img-bottom,.card-group>.card:not(:first-child) .card-footer{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:0.75rem}@media (min-width: 576px){.card-columns{-webkit-column-count:3;column-count:3;-webkit-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:0.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#444;border-radius:0.25rem}.breadcrumb-item{display:-webkit-box;display:-ms-flexbox;display:flex}.breadcrumb-item+.breadcrumb-item{padding-left:0.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:0.5rem;color:#888;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#888}.pagination{display:-webkit-box;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:0.25rem}.page-link{position:relative;display:block;padding:0.5rem 0.75rem;margin-left:0;line-height:1.25;color:#fff;background-color:#00bc8c;border:0 solid transparent}.page-link:hover{z-index:2;color:#fff;text-decoration:none;background-color:#00efb2;border-color:transparent}.page-link:focus{z-index:3;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem}.page-item:last-child .page-link{border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#00efb2;border-color:transparent}.page-item.disabled .page-link{color:#fff;pointer-events:none;cursor:auto;background-color:#007053;border-color:transparent}.pagination-lg .page-link{padding:0.75rem 1.5rem;font-size:1.171875rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:0.3rem;border-bottom-left-radius:0.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:0.3rem;border-bottom-right-radius:0.3rem}.pagination-sm .page-link{padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:0.2rem;border-bottom-left-radius:0.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:0.2rem;border-bottom-right-radius:0.2rem}.badge{display:inline-block;padding:0.25em 0.4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:0.25rem;-webkit-transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.badge{-webkit-transition:none;transition:none}}a.badge:hover,a.badge:focus{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:0.6em;padding-left:0.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#375a7f}a.badge-primary:hover,a.badge-primary:focus{color:#fff;background-color:#28415b}a.badge-primary:focus,a.badge-primary.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5)}.badge-secondary{color:#fff;background-color:#444}a.badge-secondary:hover,a.badge-secondary:focus{color:#fff;background-color:#2b2a2a}a.badge-secondary:focus,a.badge-secondary.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5);box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5)}.badge-success{color:#fff;background-color:#00bc8c}a.badge-success:hover,a.badge-success:focus{color:#fff;background-color:#008966}a.badge-success:focus,a.badge-success.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5)}.badge-info{color:#fff;background-color:#3498DB}a.badge-info:hover,a.badge-info:focus{color:#fff;background-color:#217dbb}a.badge-info:focus,a.badge-info.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5);box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5)}.badge-warning{color:#fff;background-color:#F39C12}a.badge-warning:hover,a.badge-warning:focus{color:#fff;background-color:#c87f0a}a.badge-warning:focus,a.badge-warning.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5);box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5)}.badge-danger{color:#fff;background-color:#E74C3C}a.badge-danger:hover,a.badge-danger:focus{color:#fff;background-color:#d62c1a}a.badge-danger:focus,a.badge-danger.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5)}.badge-light{color:#222;background-color:#adb5bd}a.badge-light:hover,a.badge-light:focus{color:#222;background-color:#919ca6}a.badge-light:focus,a.badge-light.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5);box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5)}.badge-dark{color:#fff;background-color:#303030}a.badge-dark:hover,a.badge-dark:focus{color:#fff;background-color:#171616}a.badge-dark:focus,a.badge-dark.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5);box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#303030;border-radius:0.3rem}@media (min-width: 576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:0.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:0.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3.90625rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:0.75rem 1.25rem;color:inherit}.alert-primary{color:#1d2f42;background-color:#d7dee5;border-color:#c7d1db}.alert-primary hr{border-top-color:#b7c4d1}.alert-primary .alert-link{color:#0d161f}.alert-secondary{color:#232323;background-color:#dadada;border-color:#cbcbcb}.alert-secondary hr{border-top-color:#bebebe}.alert-secondary .alert-link{color:#0a0909}.alert-success{color:#006249;background-color:#ccf2e8;border-color:#b8ecdf}.alert-success hr{border-top-color:#a4e7d6}.alert-success .alert-link{color:#002f23}.alert-info{color:#1b4f72;background-color:#d6eaf8;border-color:#c6e2f5}.alert-info hr{border-top-color:#b0d7f1}.alert-info .alert-link{color:#113249}.alert-warning{color:#7e5109;background-color:#fdebd0;border-color:#fce3bd}.alert-warning hr{border-top-color:#fbd9a5}.alert-warning .alert-link{color:#4e3206}.alert-danger{color:#78281f;background-color:#fadbd8;border-color:#f8cdc8}.alert-danger hr{border-top-color:#f5b8b1}.alert-danger .alert-link{color:#4f1a15}.alert-light{color:#5a5e62;background-color:#eff0f2;border-color:#e8eaed}.alert-light hr{border-top-color:#dadde2}.alert-light .alert-link{color:#424547}.alert-dark{color:#191919;background-color:#d6d6d6;border-color:#c5c5c5}.alert-dark hr{border-top-color:#b8b8b8}.alert-dark .alert-link{color:black}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-webkit-box;display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;line-height:0;font-size:0.703125rem;background-color:#444;border-radius:0.25rem}.progress-bar{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#375a7f;-webkit-transition:width 0.6s ease;transition:width 0.6s ease}@media (prefers-reduced-motion: reduce){.progress-bar{-webkit-transition:none;transition:none}}.progress-bar-striped{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:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion: reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-box-flex:1;-ms-flex:1;flex:1}.list-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:0.25rem}.list-group-item-action{width:100%;color:#444;text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:#444;text-decoration:none;background-color:#444}.list-group-item-action:active{color:#fff;background-color:#ebebeb}.list-group-item{position:relative;display:block;padding:0.75rem 1.25rem;background-color:#303030;border:1px solid #444}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#888;pointer-events:none;background-color:#303030}.list-group-item.active{z-index:2;color:#fff;background-color:#375a7f;border-color:#375a7f}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width: 576px){.list-group-horizontal-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 768px){.list-group-horizontal-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 992px){.list-group-horizontal-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 1200px){.list-group-horizontal-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#1d2f42;background-color:#c7d1db}.list-group-item-primary.list-group-item-action:hover,.list-group-item-primary.list-group-item-action:focus{color:#1d2f42;background-color:#b7c4d1}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#1d2f42;border-color:#1d2f42}.list-group-item-secondary{color:#232323;background-color:#cbcbcb}.list-group-item-secondary.list-group-item-action:hover,.list-group-item-secondary.list-group-item-action:focus{color:#232323;background-color:#bebebe}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#232323;border-color:#232323}.list-group-item-success{color:#006249;background-color:#b8ecdf}.list-group-item-success.list-group-item-action:hover,.list-group-item-success.list-group-item-action:focus{color:#006249;background-color:#a4e7d6}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#006249;border-color:#006249}.list-group-item-info{color:#1b4f72;background-color:#c6e2f5}.list-group-item-info.list-group-item-action:hover,.list-group-item-info.list-group-item-action:focus{color:#1b4f72;background-color:#b0d7f1}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#1b4f72;border-color:#1b4f72}.list-group-item-warning{color:#7e5109;background-color:#fce3bd}.list-group-item-warning.list-group-item-action:hover,.list-group-item-warning.list-group-item-action:focus{color:#7e5109;background-color:#fbd9a5}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#7e5109;border-color:#7e5109}.list-group-item-danger{color:#78281f;background-color:#f8cdc8}.list-group-item-danger.list-group-item-action:hover,.list-group-item-danger.list-group-item-action:focus{color:#78281f;background-color:#f5b8b1}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#78281f;border-color:#78281f}.list-group-item-light{color:#5a5e62;background-color:#e8eaed}.list-group-item-light.list-group-item-action:hover,.list-group-item-light.list-group-item-action:focus{color:#5a5e62;background-color:#dadde2}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#5a5e62;border-color:#5a5e62}.list-group-item-dark{color:#191919;background-color:#c5c5c5}.list-group-item-dark.list-group-item-action:hover,.list-group-item-dark.list-group-item-action:focus{color:#191919;background-color:#b8b8b8}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#191919;border-color:#191919}.close{float:right;font-size:1.40625rem;font-weight:700;line-height:1;color:#fff;text-shadow:none;opacity:.5}.close:hover{color:#fff;text-decoration:none}.close:not(:disabled):not(.disabled):hover,.close:not(:disabled):not(.disabled):focus{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:0.875rem;background-color:#444;background-clip:padding-box;border:1px solid rgba(0,0,0,0.1);-webkit-box-shadow:0 0.25rem 0.75rem rgba(0,0,0,0.1);box-shadow:0 0.25rem 0.75rem rgba(0,0,0,0.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:0.25rem}.toast:not(:last-child){margin-bottom:0.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:0.25rem 0.75rem;color:#888;background-color:#303030;background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,0.05)}.toast-body{padding:0.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:0.5rem;pointer-events:none}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform 0.3s ease-out;transition:-webkit-transform 0.3s ease-out;transition:transform 0.3s ease-out;transition:transform 0.3s ease-out, -webkit-transform 0.3s ease-out;-webkit-transform:translate(0, -50px);transform:translate(0, -50px)}@media (prefers-reduced-motion: reduce){.modal.fade .modal-dialog{-webkit-transition:none;transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-webkit-box;display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-header,.modal-dialog-scrollable .modal-footer{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:-webkit-min-content;height:-moz-min-content;height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#303030;background-clip:padding-box;border:1px solid #444;border-radius:0.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:0.5}.modal-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #444;border-top-left-radius:calc(0.3rem - 1px);border-top-right-radius:calc(0.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;padding:0.75rem;border-top:1px solid #444;border-bottom-right-radius:calc(0.3rem - 1px);border-bottom-left-radius:calc(0.3rem - 1px)}.modal-footer>*{margin:0.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width: 576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:-webkit-min-content;height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width: 992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width: 1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:"Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.8203125rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:0.9}.tooltip .arrow{position:absolute;display:block;width:0.8rem;height:0.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-top,.bs-tooltip-auto[x-placement^="top"]{padding:0.4rem 0}.bs-tooltip-top .arrow,.bs-tooltip-auto[x-placement^="top"] .arrow{bottom:0}.bs-tooltip-top .arrow::before,.bs-tooltip-auto[x-placement^="top"] .arrow::before{top:0;border-width:0.4rem 0.4rem 0;border-top-color:#000}.bs-tooltip-right,.bs-tooltip-auto[x-placement^="right"]{padding:0 0.4rem}.bs-tooltip-right .arrow,.bs-tooltip-auto[x-placement^="right"] .arrow{left:0;width:0.4rem;height:0.8rem}.bs-tooltip-right .arrow::before,.bs-tooltip-auto[x-placement^="right"] .arrow::before{right:0;border-width:0.4rem 0.4rem 0.4rem 0;border-right-color:#000}.bs-tooltip-bottom,.bs-tooltip-auto[x-placement^="bottom"]{padding:0.4rem 0}.bs-tooltip-bottom .arrow,.bs-tooltip-auto[x-placement^="bottom"] .arrow{top:0}.bs-tooltip-bottom .arrow::before,.bs-tooltip-auto[x-placement^="bottom"] .arrow::before{bottom:0;border-width:0 0.4rem 0.4rem;border-bottom-color:#000}.bs-tooltip-left,.bs-tooltip-auto[x-placement^="left"]{padding:0 0.4rem}.bs-tooltip-left .arrow,.bs-tooltip-auto[x-placement^="left"] .arrow{right:0;width:0.4rem;height:0.8rem}.bs-tooltip-left .arrow::before,.bs-tooltip-auto[x-placement^="left"] .arrow::before{left:0;border-width:0.4rem 0 0.4rem 0.4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:0.25rem 0.5rem;color:#fff;text-align:center;background-color:#000;border-radius:0.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:"Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.8203125rem;word-wrap:break-word;background-color:#303030;background-clip:padding-box;border:1px solid rgba(0,0,0,0.2);border-radius:0.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:0.5rem;margin:0 0.3rem}.popover .arrow::before,.popover .arrow::after{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-top,.bs-popover-auto[x-placement^="top"]{margin-bottom:0.5rem}.bs-popover-top>.arrow,.bs-popover-auto[x-placement^="top"]>.arrow{bottom:calc(-0.5rem - 1px)}.bs-popover-top>.arrow::before,.bs-popover-auto[x-placement^="top"]>.arrow::before{bottom:0;border-width:0.5rem 0.5rem 0;border-top-color:rgba(0,0,0,0.25)}.bs-popover-top>.arrow::after,.bs-popover-auto[x-placement^="top"]>.arrow::after{bottom:1px;border-width:0.5rem 0.5rem 0;border-top-color:#303030}.bs-popover-right,.bs-popover-auto[x-placement^="right"]{margin-left:0.5rem}.bs-popover-right>.arrow,.bs-popover-auto[x-placement^="right"]>.arrow{left:calc(-0.5rem - 1px);width:0.5rem;height:1rem;margin:0.3rem 0}.bs-popover-right>.arrow::before,.bs-popover-auto[x-placement^="right"]>.arrow::before{left:0;border-width:0.5rem 0.5rem 0.5rem 0;border-right-color:rgba(0,0,0,0.25)}.bs-popover-right>.arrow::after,.bs-popover-auto[x-placement^="right"]>.arrow::after{left:1px;border-width:0.5rem 0.5rem 0.5rem 0;border-right-color:#303030}.bs-popover-bottom,.bs-popover-auto[x-placement^="bottom"]{margin-top:0.5rem}.bs-popover-bottom>.arrow,.bs-popover-auto[x-placement^="bottom"]>.arrow{top:calc(-0.5rem - 1px)}.bs-popover-bottom>.arrow::before,.bs-popover-auto[x-placement^="bottom"]>.arrow::before{top:0;border-width:0 0.5rem 0.5rem 0.5rem;border-bottom-color:rgba(0,0,0,0.25)}.bs-popover-bottom>.arrow::after,.bs-popover-auto[x-placement^="bottom"]>.arrow::after{top:1px;border-width:0 0.5rem 0.5rem 0.5rem;border-bottom-color:#303030}.bs-popover-bottom .popover-header::before,.bs-popover-auto[x-placement^="bottom"] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-0.5rem;content:"";border-bottom:1px solid #444}.bs-popover-left,.bs-popover-auto[x-placement^="left"]{margin-right:0.5rem}.bs-popover-left>.arrow,.bs-popover-auto[x-placement^="left"]>.arrow{right:calc(-0.5rem - 1px);width:0.5rem;height:1rem;margin:0.3rem 0}.bs-popover-left>.arrow::before,.bs-popover-auto[x-placement^="left"]>.arrow::before{right:0;border-width:0.5rem 0 0.5rem 0.5rem;border-left-color:rgba(0,0,0,0.25)}.bs-popover-left>.arrow::after,.bs-popover-auto[x-placement^="left"]>.arrow::after{right:1px;border-width:0.5rem 0 0.5rem 0.5rem;border-left-color:#303030}.popover-header{padding:0.5rem 0.75rem;margin-bottom:0;font-size:0.9375rem;background-color:#444;border-bottom:1px solid #373737;border-top-left-radius:calc(0.3rem - 1px);border-top-right-radius:calc(0.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:0.5rem 0.75rem;color:#fff}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transition:-webkit-transform 0.6s ease-in-out;transition:-webkit-transform 0.6s ease-in-out;transition:transform 0.6s ease-in-out;transition:transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out}@media (prefers-reduced-motion: reduce){.carousel-item{-webkit-transition:none;transition:none}}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-left),.active.carousel-item-right{-webkit-transform:translateX(100%);transform:translateX(100%)}.carousel-item-prev:not(.carousel-item-right),.active.carousel-item-left{-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;-webkit-transition-property:opacity;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;-webkit-transition:opacity 0s 0.6s;transition:opacity 0s 0.6s}@media (prefers-reduced-motion: reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{-webkit-transition:none;transition:none}}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:0.5;-webkit-transition:opacity 0.15s ease;transition:opacity 0.15s ease}@media (prefers-reduced-motion: reduce){.carousel-control-prev,.carousel-control-next{-webkit-transition:none;transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:0.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50% / 100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{-webkit-box-sizing:content-box;box-sizing:content-box;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;-webkit-transition:opacity 0.6s ease;transition:opacity 0.6s ease}@media (prefers-reduced-motion: reduce){.carousel-indicators li{-webkit-transition:none;transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:0.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:0.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.bg-primary{background-color:#375a7f !important}a.bg-primary:hover,a.bg-primary:focus,button.bg-primary:hover,button.bg-primary:focus{background-color:#28415b !important}.bg-secondary{background-color:#444 !important}a.bg-secondary:hover,a.bg-secondary:focus,button.bg-secondary:hover,button.bg-secondary:focus{background-color:#2b2a2a !important}.bg-success{background-color:#00bc8c !important}a.bg-success:hover,a.bg-success:focus,button.bg-success:hover,button.bg-success:focus{background-color:#008966 !important}.bg-info{background-color:#3498DB !important}a.bg-info:hover,a.bg-info:focus,button.bg-info:hover,button.bg-info:focus{background-color:#217dbb !important}.bg-warning{background-color:#F39C12 !important}a.bg-warning:hover,a.bg-warning:focus,button.bg-warning:hover,button.bg-warning:focus{background-color:#c87f0a !important}.bg-danger{background-color:#E74C3C !important}a.bg-danger:hover,a.bg-danger:focus,button.bg-danger:hover,button.bg-danger:focus{background-color:#d62c1a !important}.bg-light{background-color:#adb5bd !important}a.bg-light:hover,a.bg-light:focus,button.bg-light:hover,button.bg-light:focus{background-color:#919ca6 !important}.bg-dark{background-color:#303030 !important}a.bg-dark:hover,a.bg-dark:focus,button.bg-dark:hover,button.bg-dark:focus{background-color:#171616 !important}.bg-white{background-color:#fff !important}.bg-transparent{background-color:transparent !important}.border{border:1px solid #dee2e6 !important}.border-top{border-top:1px solid #dee2e6 !important}.border-right{border-right:1px solid #dee2e6 !important}.border-bottom{border-bottom:1px solid #dee2e6 !important}.border-left{border-left:1px solid #dee2e6 !important}.border-0{border:0 !important}.border-top-0{border-top:0 !important}.border-right-0{border-right:0 !important}.border-bottom-0{border-bottom:0 !important}.border-left-0{border-left:0 !important}.border-primary{border-color:#375a7f !important}.border-secondary{border-color:#444 !important}.border-success{border-color:#00bc8c !important}.border-info{border-color:#3498DB !important}.border-warning{border-color:#F39C12 !important}.border-danger{border-color:#E74C3C !important}.border-light{border-color:#adb5bd !important}.border-dark{border-color:#303030 !important}.border-white{border-color:#fff !important}.rounded-sm{border-radius:0.2rem !important}.rounded{border-radius:0.25rem !important}.rounded-top{border-top-left-radius:0.25rem !important;border-top-right-radius:0.25rem !important}.rounded-right{border-top-right-radius:0.25rem !important;border-bottom-right-radius:0.25rem !important}.rounded-bottom{border-bottom-right-radius:0.25rem !important;border-bottom-left-radius:0.25rem !important}.rounded-left{border-top-left-radius:0.25rem !important;border-bottom-left-radius:0.25rem !important}.rounded-lg{border-radius:0.3rem !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.rounded-0{border-radius:0 !important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}@media (min-width: 576px){.d-sm-none{display:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-sm-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 768px){.d-md-none{display:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-md-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 992px){.d-lg-none{display:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-lg-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 1200px){.d-xl-none{display:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-xl-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media print{.d-print-none{display:none !important}.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-print-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.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-21by9::before{padding-top:42.8571428571%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}@media (min-width: 576px){.flex-sm-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-sm-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-sm-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-sm-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-sm-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-sm-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-sm-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-sm-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-sm-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-sm-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-sm-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-sm-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-sm-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-sm-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-sm-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-sm-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-sm-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-sm-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-sm-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-sm-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-sm-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-sm-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-sm-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-sm-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-sm-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-sm-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-sm-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-sm-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-sm-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-sm-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-sm-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-sm-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-sm-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 768px){.flex-md-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-md-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-md-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-md-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-md-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-md-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-md-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-md-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-md-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-md-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-md-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-md-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-md-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-md-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-md-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-md-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-md-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-md-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-md-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-md-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-md-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-md-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-md-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-md-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-md-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-md-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-md-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-md-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-md-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-md-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-md-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-md-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-md-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 992px){.flex-lg-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-lg-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-lg-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-lg-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-lg-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-lg-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-lg-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-lg-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-lg-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-lg-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-lg-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-lg-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-lg-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-lg-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-lg-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-lg-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-lg-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-lg-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-lg-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-lg-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-lg-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-lg-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-lg-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-lg-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-lg-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-lg-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-lg-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-lg-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-lg-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-lg-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-lg-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-lg-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-lg-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 1200px){.flex-xl-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-xl-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-xl-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-xl-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-xl-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-xl-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-xl-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-xl-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-xl-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-xl-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-xl-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-xl-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-xl-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-xl-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-xl-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-xl-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-xl-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-xl-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-xl-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-xl-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-xl-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-xl-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-xl-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-xl-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-xl-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-xl-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-xl-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-xl-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-xl-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-xl-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-xl-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-xl-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-xl-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}.float-left{float:left !important}.float-right{float:right !important}.float-none{float:none !important}@media (min-width: 576px){.float-sm-left{float:left !important}.float-sm-right{float:right !important}.float-sm-none{float:none !important}}@media (min-width: 768px){.float-md-left{float:left !important}.float-md-right{float:right !important}.float-md-none{float:none !important}}@media (min-width: 992px){.float-lg-left{float:left !important}.float-lg-right{float:right !important}.float-lg-none{float:none !important}}@media (min-width: 1200px){.float-xl-left{float:left !important}.float-xl-right{float:right !important}.float-xl-none{float:none !important}}.user-select-all{-webkit-user-select:all !important;-moz-user-select:all !important;-ms-user-select:all !important;user-select:all !important}.user-select-auto{-webkit-user-select:auto !important;-moz-user-select:auto !important;-ms-user-select:auto !important;user-select:auto !important}.user-select-none{-webkit-user-select:none !important;-moz-user-select:none !important;-ms-user-select:none !important;user-select:none !important}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:-webkit-sticky !important;position:sticky !important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports (position: -webkit-sticky) or (position: sticky){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{-webkit-box-shadow:0 0.125rem 0.25rem rgba(0,0,0,0.075) !important;box-shadow:0 0.125rem 0.25rem rgba(0,0,0,0.075) !important}.shadow{-webkit-box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important;box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important}.shadow-lg{-webkit-box-shadow:0 1rem 3rem rgba(0,0,0,0.175) !important;box-shadow:0 1rem 3rem rgba(0,0,0,0.175) !important}.shadow-none{-webkit-box-shadow:none !important;box-shadow:none !important}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mw-100{max-width:100% !important}.mh-100{max-height:100% !important}.min-vw-100{min-width:100vw !important}.min-vh-100{min-height:100vh !important}.vw-100{width:100vw !important}.vh-100{height:100vh !important}.m-0{margin:0 !important}.mt-0,.my-0{margin-top:0 !important}.mr-0,.mx-0{margin-right:0 !important}.mb-0,.my-0{margin-bottom:0 !important}.ml-0,.mx-0{margin-left:0 !important}.m-1{margin:0.25rem !important}.mt-1,.my-1{margin-top:0.25rem !important}.mr-1,.mx-1{margin-right:0.25rem !important}.mb-1,.my-1{margin-bottom:0.25rem !important}.ml-1,.mx-1{margin-left:0.25rem !important}.m-2{margin:0.5rem !important}.mt-2,.my-2{margin-top:0.5rem !important}.mr-2,.mx-2{margin-right:0.5rem !important}.mb-2,.my-2{margin-bottom:0.5rem !important}.ml-2,.mx-2{margin-left:0.5rem !important}.m-3{margin:1rem !important}.mt-3,.my-3{margin-top:1rem !important}.mr-3,.mx-3{margin-right:1rem !important}.mb-3,.my-3{margin-bottom:1rem !important}.ml-3,.mx-3{margin-left:1rem !important}.m-4{margin:1.5rem !important}.mt-4,.my-4{margin-top:1.5rem !important}.mr-4,.mx-4{margin-right:1.5rem !important}.mb-4,.my-4{margin-bottom:1.5rem !important}.ml-4,.mx-4{margin-left:1.5rem !important}.m-5{margin:3rem !important}.mt-5,.my-5{margin-top:3rem !important}.mr-5,.mx-5{margin-right:3rem !important}.mb-5,.my-5{margin-bottom:3rem !important}.ml-5,.mx-5{margin-left:3rem !important}.p-0{padding:0 !important}.pt-0,.py-0{padding-top:0 !important}.pr-0,.px-0{padding-right:0 !important}.pb-0,.py-0{padding-bottom:0 !important}.pl-0,.px-0{padding-left:0 !important}.p-1{padding:0.25rem !important}.pt-1,.py-1{padding-top:0.25rem !important}.pr-1,.px-1{padding-right:0.25rem !important}.pb-1,.py-1{padding-bottom:0.25rem !important}.pl-1,.px-1{padding-left:0.25rem !important}.p-2{padding:0.5rem !important}.pt-2,.py-2{padding-top:0.5rem !important}.pr-2,.px-2{padding-right:0.5rem !important}.pb-2,.py-2{padding-bottom:0.5rem !important}.pl-2,.px-2{padding-left:0.5rem !important}.p-3{padding:1rem !important}.pt-3,.py-3{padding-top:1rem !important}.pr-3,.px-3{padding-right:1rem !important}.pb-3,.py-3{padding-bottom:1rem !important}.pl-3,.px-3{padding-left:1rem !important}.p-4{padding:1.5rem !important}.pt-4,.py-4{padding-top:1.5rem !important}.pr-4,.px-4{padding-right:1.5rem !important}.pb-4,.py-4{padding-bottom:1.5rem !important}.pl-4,.px-4{padding-left:1.5rem !important}.p-5{padding:3rem !important}.pt-5,.py-5{padding-top:3rem !important}.pr-5,.px-5{padding-right:3rem !important}.pb-5,.py-5{padding-bottom:3rem !important}.pl-5,.px-5{padding-left:3rem !important}.m-n1{margin:-0.25rem !important}.mt-n1,.my-n1{margin-top:-0.25rem !important}.mr-n1,.mx-n1{margin-right:-0.25rem !important}.mb-n1,.my-n1{margin-bottom:-0.25rem !important}.ml-n1,.mx-n1{margin-left:-0.25rem !important}.m-n2{margin:-0.5rem !important}.mt-n2,.my-n2{margin-top:-0.5rem !important}.mr-n2,.mx-n2{margin-right:-0.5rem !important}.mb-n2,.my-n2{margin-bottom:-0.5rem !important}.ml-n2,.mx-n2{margin-left:-0.5rem !important}.m-n3{margin:-1rem !important}.mt-n3,.my-n3{margin-top:-1rem !important}.mr-n3,.mx-n3{margin-right:-1rem !important}.mb-n3,.my-n3{margin-bottom:-1rem !important}.ml-n3,.mx-n3{margin-left:-1rem !important}.m-n4{margin:-1.5rem !important}.mt-n4,.my-n4{margin-top:-1.5rem !important}.mr-n4,.mx-n4{margin-right:-1.5rem !important}.mb-n4,.my-n4{margin-bottom:-1.5rem !important}.ml-n4,.mx-n4{margin-left:-1.5rem !important}.m-n5{margin:-3rem !important}.mt-n5,.my-n5{margin-top:-3rem !important}.mr-n5,.mx-n5{margin-right:-3rem !important}.mb-n5,.my-n5{margin-bottom:-3rem !important}.ml-n5,.mx-n5{margin-left:-3rem !important}.m-auto{margin:auto !important}.mt-auto,.my-auto{margin-top:auto !important}.mr-auto,.mx-auto{margin-right:auto !important}.mb-auto,.my-auto{margin-bottom:auto !important}.ml-auto,.mx-auto{margin-left:auto !important}@media (min-width: 576px){.m-sm-0{margin:0 !important}.mt-sm-0,.my-sm-0{margin-top:0 !important}.mr-sm-0,.mx-sm-0{margin-right:0 !important}.mb-sm-0,.my-sm-0{margin-bottom:0 !important}.ml-sm-0,.mx-sm-0{margin-left:0 !important}.m-sm-1{margin:0.25rem !important}.mt-sm-1,.my-sm-1{margin-top:0.25rem !important}.mr-sm-1,.mx-sm-1{margin-right:0.25rem !important}.mb-sm-1,.my-sm-1{margin-bottom:0.25rem !important}.ml-sm-1,.mx-sm-1{margin-left:0.25rem !important}.m-sm-2{margin:0.5rem !important}.mt-sm-2,.my-sm-2{margin-top:0.5rem !important}.mr-sm-2,.mx-sm-2{margin-right:0.5rem !important}.mb-sm-2,.my-sm-2{margin-bottom:0.5rem !important}.ml-sm-2,.mx-sm-2{margin-left:0.5rem !important}.m-sm-3{margin:1rem !important}.mt-sm-3,.my-sm-3{margin-top:1rem !important}.mr-sm-3,.mx-sm-3{margin-right:1rem !important}.mb-sm-3,.my-sm-3{margin-bottom:1rem !important}.ml-sm-3,.mx-sm-3{margin-left:1rem !important}.m-sm-4{margin:1.5rem !important}.mt-sm-4,.my-sm-4{margin-top:1.5rem !important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem !important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem !important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem !important}.m-sm-5{margin:3rem !important}.mt-sm-5,.my-sm-5{margin-top:3rem !important}.mr-sm-5,.mx-sm-5{margin-right:3rem !important}.mb-sm-5,.my-sm-5{margin-bottom:3rem !important}.ml-sm-5,.mx-sm-5{margin-left:3rem !important}.p-sm-0{padding:0 !important}.pt-sm-0,.py-sm-0{padding-top:0 !important}.pr-sm-0,.px-sm-0{padding-right:0 !important}.pb-sm-0,.py-sm-0{padding-bottom:0 !important}.pl-sm-0,.px-sm-0{padding-left:0 !important}.p-sm-1{padding:0.25rem !important}.pt-sm-1,.py-sm-1{padding-top:0.25rem !important}.pr-sm-1,.px-sm-1{padding-right:0.25rem !important}.pb-sm-1,.py-sm-1{padding-bottom:0.25rem !important}.pl-sm-1,.px-sm-1{padding-left:0.25rem !important}.p-sm-2{padding:0.5rem !important}.pt-sm-2,.py-sm-2{padding-top:0.5rem !important}.pr-sm-2,.px-sm-2{padding-right:0.5rem !important}.pb-sm-2,.py-sm-2{padding-bottom:0.5rem !important}.pl-sm-2,.px-sm-2{padding-left:0.5rem !important}.p-sm-3{padding:1rem !important}.pt-sm-3,.py-sm-3{padding-top:1rem !important}.pr-sm-3,.px-sm-3{padding-right:1rem !important}.pb-sm-3,.py-sm-3{padding-bottom:1rem !important}.pl-sm-3,.px-sm-3{padding-left:1rem !important}.p-sm-4{padding:1.5rem !important}.pt-sm-4,.py-sm-4{padding-top:1.5rem !important}.pr-sm-4,.px-sm-4{padding-right:1.5rem !important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem !important}.pl-sm-4,.px-sm-4{padding-left:1.5rem !important}.p-sm-5{padding:3rem !important}.pt-sm-5,.py-sm-5{padding-top:3rem !important}.pr-sm-5,.px-sm-5{padding-right:3rem !important}.pb-sm-5,.py-sm-5{padding-bottom:3rem !important}.pl-sm-5,.px-sm-5{padding-left:3rem !important}.m-sm-n1{margin:-0.25rem !important}.mt-sm-n1,.my-sm-n1{margin-top:-0.25rem !important}.mr-sm-n1,.mx-sm-n1{margin-right:-0.25rem !important}.mb-sm-n1,.my-sm-n1{margin-bottom:-0.25rem !important}.ml-sm-n1,.mx-sm-n1{margin-left:-0.25rem !important}.m-sm-n2{margin:-0.5rem !important}.mt-sm-n2,.my-sm-n2{margin-top:-0.5rem !important}.mr-sm-n2,.mx-sm-n2{margin-right:-0.5rem !important}.mb-sm-n2,.my-sm-n2{margin-bottom:-0.5rem !important}.ml-sm-n2,.mx-sm-n2{margin-left:-0.5rem !important}.m-sm-n3{margin:-1rem !important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem !important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem !important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem !important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem !important}.m-sm-n4{margin:-1.5rem !important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem !important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem !important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem !important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem !important}.m-sm-n5{margin:-3rem !important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem !important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem !important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem !important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem !important}.m-sm-auto{margin:auto !important}.mt-sm-auto,.my-sm-auto{margin-top:auto !important}.mr-sm-auto,.mx-sm-auto{margin-right:auto !important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto !important}.ml-sm-auto,.mx-sm-auto{margin-left:auto !important}}@media (min-width: 768px){.m-md-0{margin:0 !important}.mt-md-0,.my-md-0{margin-top:0 !important}.mr-md-0,.mx-md-0{margin-right:0 !important}.mb-md-0,.my-md-0{margin-bottom:0 !important}.ml-md-0,.mx-md-0{margin-left:0 !important}.m-md-1{margin:0.25rem !important}.mt-md-1,.my-md-1{margin-top:0.25rem !important}.mr-md-1,.mx-md-1{margin-right:0.25rem !important}.mb-md-1,.my-md-1{margin-bottom:0.25rem !important}.ml-md-1,.mx-md-1{margin-left:0.25rem !important}.m-md-2{margin:0.5rem !important}.mt-md-2,.my-md-2{margin-top:0.5rem !important}.mr-md-2,.mx-md-2{margin-right:0.5rem !important}.mb-md-2,.my-md-2{margin-bottom:0.5rem !important}.ml-md-2,.mx-md-2{margin-left:0.5rem !important}.m-md-3{margin:1rem !important}.mt-md-3,.my-md-3{margin-top:1rem !important}.mr-md-3,.mx-md-3{margin-right:1rem !important}.mb-md-3,.my-md-3{margin-bottom:1rem !important}.ml-md-3,.mx-md-3{margin-left:1rem !important}.m-md-4{margin:1.5rem !important}.mt-md-4,.my-md-4{margin-top:1.5rem !important}.mr-md-4,.mx-md-4{margin-right:1.5rem !important}.mb-md-4,.my-md-4{margin-bottom:1.5rem !important}.ml-md-4,.mx-md-4{margin-left:1.5rem !important}.m-md-5{margin:3rem !important}.mt-md-5,.my-md-5{margin-top:3rem !important}.mr-md-5,.mx-md-5{margin-right:3rem !important}.mb-md-5,.my-md-5{margin-bottom:3rem !important}.ml-md-5,.mx-md-5{margin-left:3rem !important}.p-md-0{padding:0 !important}.pt-md-0,.py-md-0{padding-top:0 !important}.pr-md-0,.px-md-0{padding-right:0 !important}.pb-md-0,.py-md-0{padding-bottom:0 !important}.pl-md-0,.px-md-0{padding-left:0 !important}.p-md-1{padding:0.25rem !important}.pt-md-1,.py-md-1{padding-top:0.25rem !important}.pr-md-1,.px-md-1{padding-right:0.25rem !important}.pb-md-1,.py-md-1{padding-bottom:0.25rem !important}.pl-md-1,.px-md-1{padding-left:0.25rem !important}.p-md-2{padding:0.5rem !important}.pt-md-2,.py-md-2{padding-top:0.5rem !important}.pr-md-2,.px-md-2{padding-right:0.5rem !important}.pb-md-2,.py-md-2{padding-bottom:0.5rem !important}.pl-md-2,.px-md-2{padding-left:0.5rem !important}.p-md-3{padding:1rem !important}.pt-md-3,.py-md-3{padding-top:1rem !important}.pr-md-3,.px-md-3{padding-right:1rem !important}.pb-md-3,.py-md-3{padding-bottom:1rem !important}.pl-md-3,.px-md-3{padding-left:1rem !important}.p-md-4{padding:1.5rem !important}.pt-md-4,.py-md-4{padding-top:1.5rem !important}.pr-md-4,.px-md-4{padding-right:1.5rem !important}.pb-md-4,.py-md-4{padding-bottom:1.5rem !important}.pl-md-4,.px-md-4{padding-left:1.5rem !important}.p-md-5{padding:3rem !important}.pt-md-5,.py-md-5{padding-top:3rem !important}.pr-md-5,.px-md-5{padding-right:3rem !important}.pb-md-5,.py-md-5{padding-bottom:3rem !important}.pl-md-5,.px-md-5{padding-left:3rem !important}.m-md-n1{margin:-0.25rem !important}.mt-md-n1,.my-md-n1{margin-top:-0.25rem !important}.mr-md-n1,.mx-md-n1{margin-right:-0.25rem !important}.mb-md-n1,.my-md-n1{margin-bottom:-0.25rem !important}.ml-md-n1,.mx-md-n1{margin-left:-0.25rem !important}.m-md-n2{margin:-0.5rem !important}.mt-md-n2,.my-md-n2{margin-top:-0.5rem !important}.mr-md-n2,.mx-md-n2{margin-right:-0.5rem !important}.mb-md-n2,.my-md-n2{margin-bottom:-0.5rem !important}.ml-md-n2,.mx-md-n2{margin-left:-0.5rem !important}.m-md-n3{margin:-1rem !important}.mt-md-n3,.my-md-n3{margin-top:-1rem !important}.mr-md-n3,.mx-md-n3{margin-right:-1rem !important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem !important}.ml-md-n3,.mx-md-n3{margin-left:-1rem !important}.m-md-n4{margin:-1.5rem !important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem !important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem !important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem !important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem !important}.m-md-n5{margin:-3rem !important}.mt-md-n5,.my-md-n5{margin-top:-3rem !important}.mr-md-n5,.mx-md-n5{margin-right:-3rem !important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem !important}.ml-md-n5,.mx-md-n5{margin-left:-3rem !important}.m-md-auto{margin:auto !important}.mt-md-auto,.my-md-auto{margin-top:auto !important}.mr-md-auto,.mx-md-auto{margin-right:auto !important}.mb-md-auto,.my-md-auto{margin-bottom:auto !important}.ml-md-auto,.mx-md-auto{margin-left:auto !important}}@media (min-width: 992px){.m-lg-0{margin:0 !important}.mt-lg-0,.my-lg-0{margin-top:0 !important}.mr-lg-0,.mx-lg-0{margin-right:0 !important}.mb-lg-0,.my-lg-0{margin-bottom:0 !important}.ml-lg-0,.mx-lg-0{margin-left:0 !important}.m-lg-1{margin:0.25rem !important}.mt-lg-1,.my-lg-1{margin-top:0.25rem !important}.mr-lg-1,.mx-lg-1{margin-right:0.25rem !important}.mb-lg-1,.my-lg-1{margin-bottom:0.25rem !important}.ml-lg-1,.mx-lg-1{margin-left:0.25rem !important}.m-lg-2{margin:0.5rem !important}.mt-lg-2,.my-lg-2{margin-top:0.5rem !important}.mr-lg-2,.mx-lg-2{margin-right:0.5rem !important}.mb-lg-2,.my-lg-2{margin-bottom:0.5rem !important}.ml-lg-2,.mx-lg-2{margin-left:0.5rem !important}.m-lg-3{margin:1rem !important}.mt-lg-3,.my-lg-3{margin-top:1rem !important}.mr-lg-3,.mx-lg-3{margin-right:1rem !important}.mb-lg-3,.my-lg-3{margin-bottom:1rem !important}.ml-lg-3,.mx-lg-3{margin-left:1rem !important}.m-lg-4{margin:1.5rem !important}.mt-lg-4,.my-lg-4{margin-top:1.5rem !important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem !important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem !important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem !important}.m-lg-5{margin:3rem !important}.mt-lg-5,.my-lg-5{margin-top:3rem !important}.mr-lg-5,.mx-lg-5{margin-right:3rem !important}.mb-lg-5,.my-lg-5{margin-bottom:3rem !important}.ml-lg-5,.mx-lg-5{margin-left:3rem !important}.p-lg-0{padding:0 !important}.pt-lg-0,.py-lg-0{padding-top:0 !important}.pr-lg-0,.px-lg-0{padding-right:0 !important}.pb-lg-0,.py-lg-0{padding-bottom:0 !important}.pl-lg-0,.px-lg-0{padding-left:0 !important}.p-lg-1{padding:0.25rem !important}.pt-lg-1,.py-lg-1{padding-top:0.25rem !important}.pr-lg-1,.px-lg-1{padding-right:0.25rem !important}.pb-lg-1,.py-lg-1{padding-bottom:0.25rem !important}.pl-lg-1,.px-lg-1{padding-left:0.25rem !important}.p-lg-2{padding:0.5rem !important}.pt-lg-2,.py-lg-2{padding-top:0.5rem !important}.pr-lg-2,.px-lg-2{padding-right:0.5rem !important}.pb-lg-2,.py-lg-2{padding-bottom:0.5rem !important}.pl-lg-2,.px-lg-2{padding-left:0.5rem !important}.p-lg-3{padding:1rem !important}.pt-lg-3,.py-lg-3{padding-top:1rem !important}.pr-lg-3,.px-lg-3{padding-right:1rem !important}.pb-lg-3,.py-lg-3{padding-bottom:1rem !important}.pl-lg-3,.px-lg-3{padding-left:1rem !important}.p-lg-4{padding:1.5rem !important}.pt-lg-4,.py-lg-4{padding-top:1.5rem !important}.pr-lg-4,.px-lg-4{padding-right:1.5rem !important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem !important}.pl-lg-4,.px-lg-4{padding-left:1.5rem !important}.p-lg-5{padding:3rem !important}.pt-lg-5,.py-lg-5{padding-top:3rem !important}.pr-lg-5,.px-lg-5{padding-right:3rem !important}.pb-lg-5,.py-lg-5{padding-bottom:3rem !important}.pl-lg-5,.px-lg-5{padding-left:3rem !important}.m-lg-n1{margin:-0.25rem !important}.mt-lg-n1,.my-lg-n1{margin-top:-0.25rem !important}.mr-lg-n1,.mx-lg-n1{margin-right:-0.25rem !important}.mb-lg-n1,.my-lg-n1{margin-bottom:-0.25rem !important}.ml-lg-n1,.mx-lg-n1{margin-left:-0.25rem !important}.m-lg-n2{margin:-0.5rem !important}.mt-lg-n2,.my-lg-n2{margin-top:-0.5rem !important}.mr-lg-n2,.mx-lg-n2{margin-right:-0.5rem !important}.mb-lg-n2,.my-lg-n2{margin-bottom:-0.5rem !important}.ml-lg-n2,.mx-lg-n2{margin-left:-0.5rem !important}.m-lg-n3{margin:-1rem !important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem !important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem !important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem !important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem !important}.m-lg-n4{margin:-1.5rem !important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem !important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem !important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem !important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem !important}.m-lg-n5{margin:-3rem !important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem !important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem !important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem !important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem !important}.m-lg-auto{margin:auto !important}.mt-lg-auto,.my-lg-auto{margin-top:auto !important}.mr-lg-auto,.mx-lg-auto{margin-right:auto !important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto !important}.ml-lg-auto,.mx-lg-auto{margin-left:auto !important}}@media (min-width: 1200px){.m-xl-0{margin:0 !important}.mt-xl-0,.my-xl-0{margin-top:0 !important}.mr-xl-0,.mx-xl-0{margin-right:0 !important}.mb-xl-0,.my-xl-0{margin-bottom:0 !important}.ml-xl-0,.mx-xl-0{margin-left:0 !important}.m-xl-1{margin:0.25rem !important}.mt-xl-1,.my-xl-1{margin-top:0.25rem !important}.mr-xl-1,.mx-xl-1{margin-right:0.25rem !important}.mb-xl-1,.my-xl-1{margin-bottom:0.25rem !important}.ml-xl-1,.mx-xl-1{margin-left:0.25rem !important}.m-xl-2{margin:0.5rem !important}.mt-xl-2,.my-xl-2{margin-top:0.5rem !important}.mr-xl-2,.mx-xl-2{margin-right:0.5rem !important}.mb-xl-2,.my-xl-2{margin-bottom:0.5rem !important}.ml-xl-2,.mx-xl-2{margin-left:0.5rem !important}.m-xl-3{margin:1rem !important}.mt-xl-3,.my-xl-3{margin-top:1rem !important}.mr-xl-3,.mx-xl-3{margin-right:1rem !important}.mb-xl-3,.my-xl-3{margin-bottom:1rem !important}.ml-xl-3,.mx-xl-3{margin-left:1rem !important}.m-xl-4{margin:1.5rem !important}.mt-xl-4,.my-xl-4{margin-top:1.5rem !important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem !important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem !important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem !important}.m-xl-5{margin:3rem !important}.mt-xl-5,.my-xl-5{margin-top:3rem !important}.mr-xl-5,.mx-xl-5{margin-right:3rem !important}.mb-xl-5,.my-xl-5{margin-bottom:3rem !important}.ml-xl-5,.mx-xl-5{margin-left:3rem !important}.p-xl-0{padding:0 !important}.pt-xl-0,.py-xl-0{padding-top:0 !important}.pr-xl-0,.px-xl-0{padding-right:0 !important}.pb-xl-0,.py-xl-0{padding-bottom:0 !important}.pl-xl-0,.px-xl-0{padding-left:0 !important}.p-xl-1{padding:0.25rem !important}.pt-xl-1,.py-xl-1{padding-top:0.25rem !important}.pr-xl-1,.px-xl-1{padding-right:0.25rem !important}.pb-xl-1,.py-xl-1{padding-bottom:0.25rem !important}.pl-xl-1,.px-xl-1{padding-left:0.25rem !important}.p-xl-2{padding:0.5rem !important}.pt-xl-2,.py-xl-2{padding-top:0.5rem !important}.pr-xl-2,.px-xl-2{padding-right:0.5rem !important}.pb-xl-2,.py-xl-2{padding-bottom:0.5rem !important}.pl-xl-2,.px-xl-2{padding-left:0.5rem !important}.p-xl-3{padding:1rem !important}.pt-xl-3,.py-xl-3{padding-top:1rem !important}.pr-xl-3,.px-xl-3{padding-right:1rem !important}.pb-xl-3,.py-xl-3{padding-bottom:1rem !important}.pl-xl-3,.px-xl-3{padding-left:1rem !important}.p-xl-4{padding:1.5rem !important}.pt-xl-4,.py-xl-4{padding-top:1.5rem !important}.pr-xl-4,.px-xl-4{padding-right:1.5rem !important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem !important}.pl-xl-4,.px-xl-4{padding-left:1.5rem !important}.p-xl-5{padding:3rem !important}.pt-xl-5,.py-xl-5{padding-top:3rem !important}.pr-xl-5,.px-xl-5{padding-right:3rem !important}.pb-xl-5,.py-xl-5{padding-bottom:3rem !important}.pl-xl-5,.px-xl-5{padding-left:3rem !important}.m-xl-n1{margin:-0.25rem !important}.mt-xl-n1,.my-xl-n1{margin-top:-0.25rem !important}.mr-xl-n1,.mx-xl-n1{margin-right:-0.25rem !important}.mb-xl-n1,.my-xl-n1{margin-bottom:-0.25rem !important}.ml-xl-n1,.mx-xl-n1{margin-left:-0.25rem !important}.m-xl-n2{margin:-0.5rem !important}.mt-xl-n2,.my-xl-n2{margin-top:-0.5rem !important}.mr-xl-n2,.mx-xl-n2{margin-right:-0.5rem !important}.mb-xl-n2,.my-xl-n2{margin-bottom:-0.5rem !important}.ml-xl-n2,.mx-xl-n2{margin-left:-0.5rem !important}.m-xl-n3{margin:-1rem !important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem !important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem !important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem !important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem !important}.m-xl-n4{margin:-1.5rem !important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem !important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem !important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem !important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem !important}.m-xl-n5{margin:-3rem !important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem !important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem !important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem !important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem !important}.m-xl-auto{margin:auto !important}.mt-xl-auto,.my-xl-auto{margin-top:auto !important}.mr-xl-auto,.mx-xl-auto{margin-right:auto !important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto !important}.ml-xl-auto,.mx-xl-auto{margin-left:auto !important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important}.text-justify{text-align:justify !important}.text-wrap{white-space:normal !important}.text-nowrap{white-space:nowrap !important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left !important}.text-right{text-align:right !important}.text-center{text-align:center !important}@media (min-width: 576px){.text-sm-left{text-align:left !important}.text-sm-right{text-align:right !important}.text-sm-center{text-align:center !important}}@media (min-width: 768px){.text-md-left{text-align:left !important}.text-md-right{text-align:right !important}.text-md-center{text-align:center !important}}@media (min-width: 992px){.text-lg-left{text-align:left !important}.text-lg-right{text-align:right !important}.text-lg-center{text-align:center !important}}@media (min-width: 1200px){.text-xl-left{text-align:left !important}.text-xl-right{text-align:right !important}.text-xl-center{text-align:center !important}}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.font-weight-light{font-weight:300 !important}.font-weight-lighter{font-weight:lighter !important}.font-weight-normal{font-weight:400 !important}.font-weight-bold{font-weight:700 !important}.font-weight-bolder{font-weight:bolder !important}.font-italic{font-style:italic !important}.text-white{color:#fff !important}.text-primary{color:#375a7f !important}a.text-primary:hover,a.text-primary:focus{color:#20344a !important}.text-secondary{color:#444 !important}a.text-secondary:hover,a.text-secondary:focus{color:#1e1e1e !important}.text-success{color:#00bc8c !important}a.text-success:hover,a.text-success:focus{color:#007053 !important}.text-info{color:#3498DB !important}a.text-info:hover,a.text-info:focus{color:#1d6fa5 !important}.text-warning{color:#F39C12 !important}a.text-warning:hover,a.text-warning:focus{color:#b06f09 !important}.text-danger{color:#E74C3C !important}a.text-danger:hover,a.text-danger:focus{color:#bf2718 !important}.text-light{color:#adb5bd !important}a.text-light:hover,a.text-light:focus{color:#838f9b !important}.text-dark{color:#303030 !important}a.text-dark:hover,a.text-dark:focus{color:#0a0a0a !important}.text-body{color:#fff !important}.text-muted{color:#888 !important}.text-black-50{color:rgba(0,0,0,0.5) !important}.text-white-50{color:rgba(255,255,255,0.5) !important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none !important}.text-break{word-wrap:break-word !important}.text-reset{color:inherit !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}@media print{*,*::before,*::after{text-shadow:none !important;-webkit-box-shadow:none !important;box-shadow:none !important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap !important}pre,blockquote{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px !important}.container{min-width:992px !important}.navbar{display:none}.badge{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 #dee2e6 !important}.table-dark{color:inherit}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#444}.table .thead-dark th{color:inherit;border-color:#444}}.blockquote-footer{color:#888}.table-primary,.table-primary>th,.table-primary>td{background-color:#375a7f}.table-secondary,.table-secondary>th,.table-secondary>td{background-color:#444}.table-light,.table-light>th,.table-light>td{background-color:#adb5bd}.table-dark,.table-dark>th,.table-dark>td{background-color:#303030}.table-success,.table-success>th,.table-success>td{background-color:#00bc8c}.table-info,.table-info>th,.table-info>td{background-color:#3498DB}.table-danger,.table-danger>th,.table-danger>td{background-color:#E74C3C}.table-warning,.table-warning>th,.table-warning>td{background-color:#F39C12}.table-active,.table-active>th,.table-active>td{background-color:rgba(0,0,0,0.075)}.table-hover .table-primary:hover,.table-hover .table-primary:hover>th,.table-hover .table-primary:hover>td{background-color:#2f4d6d}.table-hover .table-secondary:hover,.table-hover .table-secondary:hover>th,.table-hover .table-secondary:hover>td{background-color:#373737}.table-hover .table-light:hover,.table-hover .table-light:hover>th,.table-hover .table-light:hover>td{background-color:#9fa8b2}.table-hover .table-dark:hover,.table-hover .table-dark:hover>th,.table-hover .table-dark:hover>td{background-color:#232323}.table-hover .table-success:hover,.table-hover .table-success:hover>th,.table-hover .table-success:hover>td{background-color:#00a379}.table-hover .table-info:hover,.table-hover .table-info:hover>th,.table-hover .table-info:hover>td{background-color:#258cd1}.table-hover .table-danger:hover,.table-hover .table-danger:hover>th,.table-hover .table-danger:hover>td{background-color:#e43725}.table-hover .table-warning:hover,.table-hover .table-warning:hover>th,.table-hover .table-warning:hover>td{background-color:#e08e0b}.table-hover .table-active:hover,.table-hover .table-active:hover>th,.table-hover .table-active:hover>td{background-color:rgba(0,0,0,0.075)}.input-group-addon{color:#fff}.nav-tabs .nav-link,.nav-tabs .nav-link.active,.nav-tabs .nav-link.active:focus,.nav-tabs .nav-link.active:hover,.nav-tabs .nav-item.open .nav-link,.nav-tabs .nav-item.open .nav-link:focus,.nav-tabs .nav-item.open .nav-link:hover,.nav-pills .nav-link,.nav-pills .nav-link.active,.nav-pills .nav-link.active:focus,.nav-pills .nav-link.active:hover,.nav-pills .nav-item.open .nav-link,.nav-pills .nav-item.open .nav-link:focus,.nav-pills .nav-item.open .nav-link:hover{color:#fff}.breadcrumb a{color:#fff}.pagination a:hover{text-decoration:none}.close{opacity:0.4}.close:hover,.close:focus{opacity:1}.alert{border:none;color:#fff}.alert a,.alert .alert-link{color:#fff;text-decoration:underline}.alert-primary{background-color:#375a7f}.alert-secondary{background-color:#444}.alert-success{background-color:#00bc8c}.alert-info{background-color:#3498DB}.alert-warning{background-color:#F39C12}.alert-danger{background-color:#E74C3C}.alert-light{background-color:#adb5bd}.alert-dark{background-color:#303030}.list-group-item-action{color:#fff}.list-group-item-action:hover,.list-group-item-action:focus{background-color:#444;color:#fff}.list-group-item-action .list-group-item-heading{color:#fff}
diff --git a/htdocs/css/login.css b/htdocs/css/login.css
new file mode 100644
index 000000000..ccd6c0265
--- /dev/null
+++ b/htdocs/css/login.css
@@ -0,0 +1,34 @@
+@import url("openwebrx-header.css");
+@import url("openwebrx-globals.css");
+
+body {
+ display: flex;
+ flex-direction: column;
+}
+
+.login-container {
+ flex: 1;
+ position: relative;
+}
+
+.login {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+
+ width: 500px;
+
+ padding: 20px;
+ border-radius: 10px;
+ border: 1px solid #575757;
+ box-shadow: 0 0 20px #000;
+}
+
+.login .btn {
+ width: 100%;
+}
+
+.btn-login {
+ height: 50px;
+}
\ No newline at end of file
diff --git a/htdocs/css/map.css b/htdocs/css/map.css
new file mode 100644
index 000000000..70702b967
--- /dev/null
+++ b/htdocs/css/map.css
@@ -0,0 +1,65 @@
+@import url("openwebrx-header.css");
+@import url("openwebrx-globals.css");
+
+body {
+ display: flex;
+ flex-direction: column;
+}
+
+.openwebrx-map {
+ flex: 1 1 auto;
+}
+
+h3 {
+ margin: 10px 0;
+ text-align: center;
+}
+
+ul {
+ margin-block-start: 5px;
+ margin-block-end: 5px;
+ padding-inline-start: 25px;
+}
+
+/* don't show the filter in it's initial position */
+.openwebrx-map-legend {
+ display: none;
+ background-color: #fff;
+ padding: 10px;
+ margin: 10px;
+ user-select: none;
+}
+
+/* show it as soon as google maps has moved it to its container */
+.openwebrx-map .openwebrx-map-legend {
+ display: block;
+}
+
+.openwebrx-map-legend ul {
+ list-style-type: none;
+ padding: 0;
+}
+
+.openwebrx-map-legend ul li {
+ cursor: pointer;
+}
+
+.openwebrx-map-legend ul li.disabled {
+ opacity: .3;
+ filter: grayscale(70%);
+}
+
+.openwebrx-map-legend li.square .illustration {
+ display: inline-block;
+ width: 30px;
+ height: 20px;
+ margin-right: 10px;
+ border-width: 2px;
+ border-style: solid;
+}
+
+.openwebrx-map-legend select {
+ background-color: #FFF;
+ border-color: #DDD;
+ padding: 5px;
+}
diff --git a/htdocs/css/openwebrx-globals.css b/htdocs/css/openwebrx-globals.css
new file mode 100644
index 000000000..575984772
--- /dev/null
+++ b/htdocs/css/openwebrx-globals.css
@@ -0,0 +1,7 @@
+html, body
+{
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
+}
diff --git a/htdocs/css/openwebrx-header.css b/htdocs/css/openwebrx-header.css
new file mode 100644
index 000000000..4e8601b71
--- /dev/null
+++ b/htdocs/css/openwebrx-header.css
@@ -0,0 +1,227 @@
+.webrx-top-container {
+ position: relative;
+ z-index:1000;
+ background-color: #575757;
+
+ background-image: url(../gfx/openwebrx-top-photo.jpg);
+ background-position-x: center;
+ background-position-y: top;
+ background-repeat: no-repeat;
+ background-size: cover;
+
+ overflow: hidden;
+}
+
+.openwebrx-description-container {
+ transition-property: height, opacity;
+ transition-duration: 1s;
+ transition-timing-function: ease-out;
+ opacity: 0;
+ height: 0;
+ /* originally, top-bar + description was 350px */
+ max-height: 283px;
+ overflow: hidden;
+}
+
+.openwebrx-description-container.expanded {
+ opacity: 1;
+ height: 283px;
+}
+
+.webrx-top-bar {
+ height:67px;
+
+ background: rgba(128, 128, 128, 0.15);
+ margin:0;
+ padding:0;
+ user-select: none;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ overflow: hidden;
+
+ display: flex;
+ flex-direction: row;
+}
+
+.webrx-top-bar > * {
+ flex: 0;
+}
+
+.webrx-top-container, .webrx-top-container * {
+ line-height: initial;
+ box-sizing: initial;
+}
+
+.webrx-top-logo {
+ width: 261px;
+ padding: 12px;
+ filter: drop-shadow(0 0 2.5px rgba(0, 0, 0, .9));
+ /* overwritten by media queries */
+ display: none;
+}
+
+.webrx-rx-avatar {
+ background-color: rgba(154, 154, 154, .5);
+ margin: 7px;
+
+ width: 46px;
+ height: 46px;
+ padding: 4px;
+ border-radius: 8px;
+ box-sizing: content-box;
+}
+
+.webrx-rx-texts {
+ /* minimum layout width */
+ width: 0;
+ /* will be getting wider with flex */
+ flex: 1;
+ overflow: hidden;
+ margin: auto 0;
+}
+
+.webrx-rx-texts div, .webrx-rx-texts h1 {
+ margin: 0 10px;
+ padding: 3px;
+ white-space:nowrap;
+ overflow: hidden;
+ color: #909090;
+ text-align: left;
+}
+
+.webrx-rx-title {
+ font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
+ font-size: 11pt;
+ font-weight: bold;
+}
+
+.webrx-rx-desc {
+ font-size: 10pt;
+}
+
+.openwebrx-main-buttons .button {
+ display: block;
+ width: 55px;
+ cursor:pointer;
+}
+
+.openwebrx-main-buttons .button[data-toggle-panel] {
+ /* will be enabled by javascript if the panel is present in the DOM */
+ display: none;
+}
+
+.openwebrx-main-buttons .button img,
+.openwebrx-main-buttons .button svg {
+ height: 38px;
+ filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.5));
+}
+
+.openwebrx-main-buttons a {
+ color: inherit;
+ text-decoration: inherit;
+}
+
+.openwebrx-main-buttons .button:hover {
+ background-color: rgba(255, 255, 255, 0.3);
+}
+
+.openwebrx-main-buttons .button:active {
+ background-color: rgba(255, 255, 255, 0.55);
+}
+
+
+.openwebrx-main-buttons {
+ padding: 5px 15px;
+ display: flex;
+ list-style: none;
+ margin:0;
+ color: white;
+ text-shadow: 0px 0px 4px #000000;
+ text-align: center;
+ font-size: 9pt;
+ font-weight: bold;
+}
+
+.webrx-rx-photo-title {
+ margin: 10px 15px;
+ color: white;
+ font-size: 16pt;
+ text-shadow: 1px 1px 4px #444;
+ opacity: 1;
+}
+
+.webrx-rx-photo-desc {
+ margin: 10px 15px;
+ color: white;
+ font-size: 10pt;
+ font-weight: bold;
+ text-shadow: 0px 0px 6px #444;
+ opacity: 1;
+ line-height: 1.5em;
+}
+
+.webrx-rx-photo-desc a {
+ color: #5ca8ff;
+ text-shadow: none;
+}
+
+.openwebrx-photo-trigger {
+ cursor: pointer;
+}
+
+/*
+ * Responsive stuff
+ */
+
+@media (min-width: 576px) {
+ .webrx-rx-texts {
+ display: initial;
+ }
+}
+
+@media (min-width: 768px) {
+}
+
+@media (min-width: 992px) {
+ .webrx-top-logo {
+ display: initial;
+ }
+}
+
+@media (min-width: 1200px) {
+}
+
+/*
+ * RX details arrow up/down switching
+ */
+
+.openwebrx-rx-details-arrow {
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ transform: translate(-50%, 0);
+
+ margin: 0;
+ padding: 0;
+ line-height: 0;
+ display: block;
+}
+
+.openwebrx-rx-details-arrow svg {
+ height: 12px;
+}
+
+.openwebrx-rx-details-arrow .up {
+ display: none;
+}
+
+.openwebrx-rx-details-arrow--up .down {
+ display: none;
+}
+
+.openwebrx-rx-details-arrow--up .up {
+ display: initial;
+}
\ No newline at end of file
diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css
new file mode 100644
index 000000000..fae763788
--- /dev/null
+++ b/htdocs/css/openwebrx.css
@@ -0,0 +1,1442 @@
+/*
+
+ This file is part of OpenWebRX,
+ an open-source SDR receiver software with a web UI.
+ Copyright (c) 2013-2015 by Andras Retzler
+ Copyright (c) 2019-2021 by Jakob Ketterl
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as
+ published by the Free Software Foundation, either version 3 of the
+ License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+*/
+@import url("openwebrx-header.css");
+@import url("openwebrx-globals.css");
+
+html, body {
+ overflow: hidden;
+}
+
+select
+{
+ font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
+}
+
+input
+{
+ vertical-align:middle;
+}
+
+input[type=range] {
+ -webkit-appearance: none;
+ margin: 0 0;
+ background: transparent !important;
+ --track-background: #B6B6B6;
+}
+
+input[type=range]:focus {
+ outline: none;
+}
+
+input[type=range]::-webkit-slider-runnable-track
+{
+ height: 5px;
+ cursor: pointer;
+ animate: 0.2s;
+ box-shadow: 0px 0px 0px #000000;
+ background: #B6B6B6;
+ /*border-radius: 11px;*/
+ border: 1px solid #8A8A8A;
+ background: var(--track-background);
+}
+
+input[type=range]::-webkit-slider-thumb
+{
+ box-shadow: 1px 1px 1px #828282;
+ border: 1px solid #8A8A8A;
+ height: 15px;
+ width: 15px;
+ border-radius: 10px;
+ background: #FFFFFF;
+ cursor: pointer;
+ -webkit-appearance: none;
+ margin-top: -7px;
+}
+
+input[type=range]:focus::-webkit-slider-runnable-track
+{
+ background: #B6B6B6;
+ background: var(--track-background);
+}
+
+input[type=range]::-moz-range-track
+{
+ height: 3px;
+ cursor: pointer;
+ animate: 0.2s;
+ box-shadow: 0px 0px 0px #000000;
+ background: #B6B6B6;
+ background: var(--track-background);
+ border-radius: 11px;
+ border: 1px solid #8A8A8A;
+}
+
+input[type=range]::-moz-range-thumb
+{
+ box-shadow: 1px 1px 1px #828282;
+ border: 1px solid #8A8A8A;
+ height: 12px;
+ width: 12px;
+ border-radius: 10px;
+ background: #FFFFFF;
+ cursor: pointer;
+}
+
+input[type=range]::-ms-track
+{
+ width: 100%;
+ height: 7px;
+ cursor: pointer;
+ animate: 0.2s;
+ background: transparent;
+ border-color: transparent;
+ color: transparent;
+}
+
+input[type=range]::-ms-fill-lower
+ {
+ background: #B6B6B6;
+ border: 1px solid #8A8A8A;
+ border-radius: 22px;
+ box-shadow: 0px 0px 0px #000000;
+}
+
+input[type=range]::-ms-fill-upper
+{
+ background: #B6B6B6;
+ border: 1px solid #8A8A8A;
+ border-radius: 22px;
+ box-shadow: 0px 0px 0px #000000;
+}
+
+input[type=range]::-ms-thumb
+{
+ box-shadow: 1px 1px 1px #828282;
+ border: 1px solid #8A8A8A;
+ height: 24px;
+ width: 7px;
+ border-radius: 0px;
+ background: #FFFFFF;
+ cursor: pointer;
+}
+
+input[type=range]:focus::-ms-fill-lower
+{
+ background: #B6B6B6;
+}
+
+input[type=range]:focus::-ms-fill-upper
+{
+ background: #B6B6B6;
+}
+
+input[type=range]:disabled {
+ opacity: 0.5;
+}
+
+#webrx-page-container
+{
+ height: 100%;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+}
+
+#openwebrx-scale-container
+{
+ height: 47px;
+ overflow: hidden;
+ z-index:1000;
+ position: relative;
+}
+
+#openwebrx-frequency-container {
+ background-image: url("../gfx/openwebrx-scale-background.png");
+ background-repeat: repeat-x;
+ background-size: cover;
+ background-color: #444;
+ z-index: 1001;
+}
+
+#openwebrx-bookmarks-container
+{
+ height: 25px;
+ position: relative;
+ z-index: 1000;
+}
+
+#openwebrx-bookmarks-container .bookmark {
+ font-size: 12px;
+ background-color: #FFFF00;
+ border: 1px solid #000;
+ border-radius: 5px;
+ padding: 2px 5px;
+ cursor: pointer;
+ white-space: nowrap;
+ max-height: 14px;
+ max-width: 50px;
+
+ position: absolute;
+ bottom: 5px;
+ transform: translate(-50%, 0);
+}
+
+#openwebrx-bookmarks-container .bookmark .bookmark-content {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+#openwebrx-bookmarks-container .bookmark .bookmark-actions {
+ display: none;
+ text-align: right;
+}
+
+.bookmark-actions .action {
+ line-height: 0;
+}
+
+.bookmark-actions .action img {
+ width: 14px;
+}
+
+#openwebrx-bookmarks-container .bookmark.selected {
+ z-index: 1010;
+}
+
+#openwebrx-bookmarks-container .bookmark:hover {
+ z-index: 1011;
+ max-height: none;
+ max-width: none;
+}
+
+#openwebrx-bookmarks-container .bookmark[editable]:hover .bookmark-actions {
+ display: block;
+ margin-bottom: 5px;
+}
+
+#openwebrx-bookmarks-container .bookmark:after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ width: 0;
+ height: 0;
+ border: 5px solid transparent;
+ border-top-color: #FFFF00;
+ border-bottom: 0;
+ margin-left: -5px;
+ margin-bottom: -5px;
+}
+
+#openwebrx-bookmarks-container .bookmark[data-source=local] {
+ background-color: #0FF;
+}
+
+#openwebrx-bookmarks-container .bookmark[data-source=local]:after {
+ border-top-color: #0FF;
+}
+
+#openwebrx-bookmarks-container .bookmark[data-source=dial_frequencies] {
+ background-color: #0F0;
+}
+
+#openwebrx-bookmarks-container .bookmark[data-source=dial_frequencies]:after {
+ border-top-color: #0F0;
+}
+
+#webrx-canvas-background {
+ flex-grow: 1;
+ background-image: url('../gfx/openwebrx-background-cool-blue.png');
+ background-repeat: no-repeat;
+ background-color: #1e5f7f;
+ background-size: cover;
+ display: flex;
+ flex-direction: column;
+}
+
+@supports(background-image: -webkit-image-set(url('../gfx/openwebrx-background-cool-blue.webp') 1x)) {
+ #webrx-canvas-background {
+ background-image: -webkit-image-set(url('../gfx/openwebrx-background-cool-blue.webp') 1x);
+ }
+}
+
+@supports(background-image: image-set(url('../gfx/openwebrx-background-cool-blue.webp') 1x)) {
+ #webrx-canvas-background {
+ background-image: image-set(url('../gfx/openwebrx-background-cool-blue.webp') 1x);
+ }
+}
+
+#webrx-canvas-container
+{
+ position: relative;
+ overflow: visible;
+ cursor: crosshair;
+ flex-grow: 1;
+}
+
+#webrx-canvas-container canvas
+{
+ position: absolute;
+ top: 0;
+ border-style: none;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ width: 100%;
+ height: 200px;
+ will-change: transform;
+}
+
+#openwebrx-log-scroll
+{
+ /*overflow-y:auto;*/
+ height: 125px;
+ width: 619px
+}
+
+.nano .nano-pane { background: #444; }
+.nano .nano-slider { background: #eee !important; }
+
+.webrx-error
+{
+ font-weight: bold;
+ color: #ff6262;
+}
+
+@font-face {
+ font-family: 'roboto-mono';
+ src: url('../fonts/RobotoMono-Regular.woff2') format('woff2'),
+ url('../fonts/RobotoMono-Regular.woff') format('woff'),
+ url('../fonts/RobotoMono-Regular.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+
+.webrx-actual-freq {
+ width: 100%;
+ text-align: left;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: row;
+ cursor: pointer;
+}
+
+.webrx-actual-freq > * {
+ flex: 1;
+}
+
+.webrx-actual-freq .input-group {
+ display: flex;
+ flex-direction: row;
+}
+
+.webrx-actual-freq .input-group > * {
+ flex: 0 0 auto;
+}
+
+.webrx-actual-freq .input-group input {
+ flex: 1 0 auto;
+ margin-right: 0;
+ border-right: 1px solid #373737;
+ -moz-appearance: textfield;
+}
+
+.webrx-actual-freq .input-group input::-webkit-outer-spin-button,
+.webrx-actual-freq .input-group input::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+
+.input-group > :not(:last-child) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.input-group > :not(:first-child) {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.input-group :first-child {
+ padding-left: 5px;
+}
+
+.input-group :last-child {
+ padding-right: 5px
+}
+
+.webrx-actual-freq .input-group input, .webrx-actual-freq .input-group select {
+ outline: none;
+ font-size: 16pt;
+}
+
+.webrx-actual-freq input {
+ font-family: 'roboto-mono';
+ width: 0;
+ box-sizing: border-box;
+ border: 0;
+ padding: 0;
+ background-color: inherit;
+ color: inherit;
+}
+
+.webrx-actual-freq, .webrx-actual-freq input {
+ font-size: 16pt;
+ font-family: 'roboto-mono';
+}
+
+.webrx-actual-freq .digit {
+ cursor: ns-resize;
+}
+
+.webrx-actual-freq .digit:hover {
+ color: #FFFF50;
+ border-radius: 5px;
+ background: -webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) );
+ background: -moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% );
+}
+
+.webrx-mouse-freq {
+ width: 100%;
+ text-align: left;
+ font-size: 10pt;
+ color: #AAA;
+ font-family: 'roboto-mono';
+ margin-bottom: 5px;
+}
+
+#openwebrx-panels-container-left,
+#openwebrx-panels-container-right {
+ position: absolute;
+ bottom: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ height: 0;
+ overflow: visible;
+}
+
+#openwebrx-panels-container-left {
+ left: 0;
+ align-items: flex-start;
+}
+
+#openwebrx-panels-container-right {
+ right: 0;
+ align-items: flex-end;
+}
+
+.openwebrx-panel
+{
+ transform: perspective( 600px ) rotateX( 90deg );
+ background-color: #575757;
+ padding: 10px;
+ color: white;
+ font-size: 10pt;
+ border-radius: 15px;
+ -moz-border-radius: 15px;
+ margin: 5.9px;
+ box-sizing: content-box;
+}
+
+.openwebrx-panel a
+{
+ color: #5ca8ff;
+ text-shadow: none;
+}
+
+.openwebrx-panel-inner
+{
+ overflow-y: auto;
+ overflow-x: hidden;
+ height: 100%;
+}
+
+.openwebrx-button
+{
+ background-color: #373737;
+ padding: 4.2px;
+ border-radius: 5px;
+ -moz-border-radius: 5px;
+ color: White;
+ font-weight: bold;
+ margin-right: 1px;
+ cursor: pointer;
+ background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) );
+ background:-moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% );
+ user-select: none;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ display: inline-block;
+}
+
+.openwebrx-button:hover, .openwebrx-demodulator-button.highlighted, .openwebrx-button.highlighted
+{
+ /*background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #3F3F3F), color-stop(1, #777777) );
+ background:-moz-linear-gradient( center top, #373737 5%, #4F4F4F 100% );*/
+ background: #474747;
+ color: #FFFF50;
+}
+
+.openwebrx-button:active
+{
+ background: #777777;
+ color: #FFFF50;
+}
+
+.openwebrx-button:last-child {
+ margin-right: 0;
+}
+
+.openwebrx-button.disabled {
+ opacity: 0.5;
+}
+
+.openwebrx-demodulator-button
+{
+ height: 19px;
+ font-size: 12pt;
+ text-align: center;
+ flex: 1;
+ margin-right: 5px;
+}
+
+.openwebrx-demodulator-button.same-mod {
+ color: #FFC;
+}
+
+.openwebrx-square-button img
+{
+ height: 27px;
+}
+
+.openwebrx-round-button
+{
+ margin-right: -2px;
+ width: 35px;
+ height: 35px;
+ border-radius: 25px;
+}
+
+.openwebrx-round-button img
+{
+ height: 30px;
+}
+
+.openwebrx-round-button-small
+{
+ margin-right: -3px;
+ width: 20px;
+ height: 20px;
+ border-radius: 25px;
+}
+
+.openwebrx-round-button-small img
+{
+ height: 20px;
+}
+
+img.openwebrx-mirror-img
+{
+ transform: scale(-1, 1);
+}
+
+
+.openwebrx-round-rightarrow img
+{
+ position: relative;
+ left: 12px;
+ top: 3px;
+}
+
+.openwebrx-round-leftarrow img
+{
+ position: relative;
+ left: 7px;
+ top: 3px;
+}
+
+#openwebrx-client-log-title
+{
+ margin-bottom: 5px;
+ font-weight: bold;
+}
+
+.openwebrx-progressbar
+{
+ position: relative;
+ border-radius: 5px;
+ background-color: #003850; /*#006235;*/
+ display: inline-block;
+ text-align: center;
+ font-size: 8pt;
+ font-weight: bold;
+ text-shadow: 0px 0px 4px #000000;
+ cursor: default;
+ user-select: none;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ overflow: hidden;
+ z-index: 1
+}
+
+.openwebrx-progressbar-bar {
+ background-color: #00aba6;
+ border-radius: 5px;
+ height: 100%;
+ width: 100%;
+ transition-property: transform, background-color;
+ transition-duration: 1s;
+ transition-timing-function: ease-in-out;
+ transform: translate(-100%) translateZ(0);
+ will-change: transform, background-color;
+ z-index: 0;
+}
+
+.openwebrx-progressbar--over .openwebrx-progressbar-bar {
+ background-color: #ff6262;
+}
+
+.openwebrx-progressbar-text
+{
+ position: absolute;
+ left:50%;
+ top:50%;
+ transform: translate(-50%, -50%);
+ white-space: nowrap;
+ z-index: 2;
+}
+
+#openwebrx-panel-status
+{
+ margin: 0 0 0 5.9px;
+ padding: 0px;
+ background-color:rgba(0, 0, 0, 0);
+}
+
+#openwebrx-panel-status div.openwebrx-progressbar
+{
+ width: 200px;
+ height: 20px;
+}
+
+#openwebrx-panel-receiver
+{
+ width:110px;
+}
+
+
+#openwebrx-panel-receiver .frequencies-container {
+ display: flex;
+ flex-direction: row;
+ gap: 5px;
+}
+
+#openwebrx-panel-receiver .frequencies {
+ flex-grow: 1;
+}
+
+#openwebrx-panel-receiver .openwebrx-bookmark-button {
+ width: 27px;
+ height: 27px;
+ text-align: center;
+}
+
+.openwebrx-panel-slider
+{
+ position: relative;
+ top: -2px;
+ width: 95px;
+}
+
+.openwebrx-panel-line
+{
+ padding-top: 5px;
+}
+
+.openwebrx-panel-flex-line {
+ display: flex;
+ flex-direction: row;
+}
+
+.openwebrx-panel-line:first-child {
+ padding-top: 0;
+}
+
+.openwebrx-modes-grid {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ margin: -5px -5px 0 0;
+}
+
+.openwebrx-modes-grid .openwebrx-demodulator-button {
+ margin: 0;
+ white-space: nowrap;
+ flex: 1 0 38px;
+ margin: 5px 5px 0 0;
+}
+
+@supports(gap: 5px) {
+ .openwebrx-modes-grid {
+ margin: 0;
+ gap: 5px;
+ }
+
+ .openwebrx-modes-grid .openwebrx-demodulator-button {
+ margin: 0;
+ }
+}
+
+#openwebrx-smeter {
+ border-color: #888;
+ border-style: solid;
+ border-width: 0px;
+ width: 255px;
+ height: 7px;
+ background-color: #373737;
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.openwebrx-smeter-bar {
+ transition-property: transform;
+ transition-duration: 0.2s;
+ transition-timing-function: linear;
+ will-change: transform;
+ transform: translate(-100%) translateZ(0);
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(to top, #ff5939 , #961700);
+ margin: 0;
+ padding: 0;
+ border-radius: 3px;
+}
+
+#openwebrx-smeter-db
+{
+ color: #aaa;
+ display: inline-block;
+ font-size: 10pt;
+ float: right;
+ margin-right: 5px;
+ margin-top: 24px;
+ font-family: 'roboto-mono';
+}
+
+.openwebrx-overlay {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ opacity: 0.8;
+ background-color: #777;
+ left: 0;
+ top: 0;
+ z-index: 1001;
+ color: white;
+ font-weight: bold;
+ font-size: 20pt;
+}
+
+#openwebrx-autoplay-overlay
+{
+ cursor: pointer;
+ transition: opacity 0.3s linear;
+}
+
+#openwebrx-autoplay-overlay svg {
+ width: 150px;
+}
+
+.openwebrx-overlay .overlay-content {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ text-align: center;
+}
+
+#openwebrx-error-overlay .overlay-content {
+ background-color: #000;
+ padding: 50px;
+ border-radius: 20px;
+}
+
+#openwebrx-digimode-canvas-container
+{
+ margin: -10px -10px -10px -10px;
+ border-radius: 15px;
+ height: 200px;
+ background-color: #333;
+ position: relative;
+ overflow: hidden;
+}
+
+#openwebrx-digimode-canvas-container canvas
+{
+ position: absolute;
+ top: 0;
+ pointer-events: none;
+ transition: width 500ms, left 500ms;
+ will-change: transform;
+}
+
+.openwebrx-panel select,
+.openwebrx-panel input,
+.openwebrx-dialog select,
+.openwebrx-dialog input {
+ border-radius: 5px;
+ background-color: #373737;
+ color: White;
+ font-weight: normal;
+ font-size: 13pt;
+ margin-right: 1px;
+ background:linear-gradient(#373737, #4F4F4F);
+ border-color: transparent;
+ border-width: 0px;
+}
+
+@supports(-moz-appearance: none) {
+ .openwebrx-panel select,
+ .openwebrx-dialog select {
+ -moz-appearance: none;
+ background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%20%20xmlns%3Av%3D%22https%3A%2F%2Fvecta.io%2Fnano%22%3E%3Cpath%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8s-1.9-9.2-5.5-12.8z%22%20fill%3D%22%23fff%22%2F%3E%3C%2Fsvg%3E'),
+ linear-gradient(#373737, #4F4F4F);
+ background-repeat: no-repeat, repeat;
+ background-position: right .3em top 50%, 0 0;
+ background-size: .65em auto, 100%;
+ }
+
+ .openwebrx-panel .input-group select,
+ .openwebrx-dialog .input-group select {
+ padding-right: 1em;
+ }
+}
+
+.openwebrx-panel select option,
+.openwebrx-dialog select option {
+ border-width: 0px;
+ background-color: #373737;
+ color: White;
+}
+
+.openwebrx-secondary-demod-listbox {
+ width: 173px;
+ height: 27px;
+ padding-left:3px;
+ flex: 4;
+}
+
+#openwebrx-sdr-profiles-listbox {
+ width: 100%;
+ font-size: 10pt;
+ height: 27px;
+}
+
+#openwebrx-cursor-blink
+{
+ animation: cursor-blink 1s infinite;
+ /*animation: cursor-3d 2s infinite;*/
+ animation-timing-function: linear;
+ animation-direction: alternate;
+ height: 1em;
+ width: 8px;
+ background-color: White;
+ display: inline-block;
+ position: relative;
+ top: 1px;
+ /*perspective: 60px;*/
+
+}
+
+@keyframes cursor-blink
+{
+ 0%{ opacity: 0; }
+ 50% { opacity: 1; }
+ 100%{ opacity: 0; }
+}
+
+@keyframes cursor-3d
+{
+ 0%{ transform: rotateX(0deg) rotateX(Ydeg); }
+ 50% { transform: rotateX(180deg) rotateY(360deg); opacity: 0.1; }
+ 100%{ transform: rotateX(360deg) rotateY(720deg); }
+}
+
+#openwebrx-digimode-content
+{
+ word-wrap: break-word;
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+}
+
+#openwebrx-digimode-content-container
+{
+ overflow-y: hidden;
+ display: none;
+ height: 50px;
+ position: relative;
+}
+
+#openwebrx-digimode-content-container .gradient
+{
+ width: 100%;
+ height: 20px;
+ background: linear-gradient(to top, rgba(87,87,87,0) 0%,rgba(87,87,87,1) 100%);
+ position: absolute;
+ top: 0;
+ z-index: 10;
+}
+
+#openwebrx-digimode-select-channel
+{
+ transition: all 500ms;
+ background-color: Yellow;
+ display: none;
+ position: absolute;
+ pointer-events: none;
+ height: 100%;
+ width: 0;
+ top: 0;
+ left: 0;
+ opacity: 0.7;
+ border-style: solid;
+ border-width: 0;
+ border-color: Red;
+}
+
+.openwebrx-meta-panel {
+ display: flex;
+ flex-direction: row;
+ gap: 10px;
+ /* compatibility with iOS 14.2 */
+ flex: 0 0 auto;
+}
+
+.openwebrx-meta-slot {
+ flex: 1;
+ width: 145px;
+ height: 196px;
+
+ background-color: #676767;
+ padding: 2px 0;
+ color: #333;
+
+ text-align: center;
+
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ overflow: hidden;
+}
+
+.openwebrx-meta-slot > * {
+ flex: 1 0 0;
+ line-height: 1.2em;
+}
+
+.openwebrx-meta-slot, .openwebrx-meta-slot .mute {
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+.openwebrx-meta-slot .mute {
+ display: none;
+ cursor: pointer;
+
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ background-color: rgba(0,0,0,.3);
+}
+
+.openwebrx-meta-slot .mute svg {
+ position: absolute;
+ top: 50%;
+ left: 0;
+ transform: translate(0, -50%);
+}
+
+.openwebrx-meta-slot.muted .mute {
+ display: block;
+}
+
+.openwebrx-meta-slot.active {
+ background-color: #95bbdf;
+}
+
+.openwebrx-meta-slot.sync .openwebrx-dmr-slot:before {
+ content:"";
+ display: inline-block;
+ margin: 0 5px;
+ width: 12px;
+ height: 12px;
+ background-color: #ABFF00;
+ border-radius: 50%;
+ box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #304701 0 -1px 9px, #89FF00 0 2px 12px;
+}
+
+.openwebrx-meta-slot .openwebrx-meta-user-image {
+ flex: 0 1 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+ line-height: 0;
+ overflow: hidden;
+}
+
+.openwebrx-meta-slot .openwebrx-meta-user-image img {
+ max-width: 100%;
+ max-height: 100%;
+ display: none;
+}
+
+.openwebrx-meta-slot.active.direct .openwebrx-meta-user-image .directcall,
+.openwebrx-meta-slot.active.individual .openwebrx-meta-user-image .directcall,
+#openwebrx-panel-metadata-ysf .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall,
+#openwebrx-panel-metadata-dstar .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall,
+#openwebrx-panel-metadata-m17 .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall {
+ display: initial;
+}
+
+.openwebrx-meta-slot.active.group .openwebrx-meta-user-image .groupcall,
+.openwebrx-meta-slot.active.conference .openwebrx-meta-user-image .groupcall {
+ display: initial;
+}
+
+.openwebrx-meta-slot.group .openwebrx-dmr-target:not(:empty):before {
+ content: "Talkgroup: ";
+}
+
+.openwebrx-meta-slot.direct .openwebrx-dmr-target:not(:empty):before {
+ content: "Direct: ";
+}
+
+.openwebrx-dmr-timeslot-panel * {
+ cursor: pointer;
+ user-select: none;
+}
+
+.openwebrx-ysf-mode:not(:empty):before {
+ content: "Mode: ";
+}
+
+.openwebrx-ysf-up:not(:empty):before {
+ content: "Up: ";
+}
+
+.openwebrx-ysf-down:not(:empty):before {
+ content: "Down: ";
+}
+
+.openwebrx-m17-source:not(:empty):before {
+ content: "SRC: ";
+}
+
+.openwebrx-m17-destination:not(:empty):before {
+ content: "DEST: ";
+}
+
+.openwebrx-dstar-yourcall:not(:empty):before {
+ content: "UR: ";
+}
+
+.openwebrx-dstar-departure:not(:empty):before {
+ content: "RPT1: ";
+}
+
+.openwebrx-dstar-destination:not(:empty):before {
+ content: "RPT2: ";
+}
+
+.openwebrx-meta-slot.individual .openwebrx-nxdn-destination:not(:empty):before {
+ content: "Direct: ";
+}
+
+.openwebrx-meta-slot.conference .openwebrx-nxdn-destination:not(:empty):before {
+ content: "Conference: ";
+}
+
+.openwebrx-maps-pin svg {
+ width: 15px;
+ height: 15px;
+ vertical-align: middle;
+}
+
+.openwebrx-message-panel {
+ min-height: 180px;
+ position: relative;
+}
+
+.openwebrx-message-panel#openwebrx-panel-adsb-message {
+ min-height: 380px;
+}
+
+.openwebrx-message-panel table {
+ display: block;
+ overflow: auto;
+ height: 100%;
+ width: 100%;
+}
+
+.openwebrx-message-panel th,
+.openwebrx-message-panel td {
+ min-width: 50px;
+ text-align: left;
+ vertical-align: top;
+ padding: 1px 3px;
+}
+
+.openwebrx-message-panel th {
+ position: sticky;
+ top: 0;
+ background-color: #575757;
+}
+
+.openwebrx-message-panel h4 {
+ margin: 0 0 .25em;
+}
+
+.openwebrx-message-panel .acars-message {
+ white-space: pre;
+ font-family: roboto-mono, monospace;
+}
+
+#openwebrx-panel-wsjt-message .message {
+ width: 380px;
+}
+
+#openwebrx-panel-wsjt-message .decimal {
+ text-align: right;
+ width: 35px;
+}
+
+#openwebrx-panel-wsjt-message .decimal.freq {
+ width: 70px;
+}
+
+#openwebrx-panel-js8-message .message {
+ width: 465px;
+ max-width: 465px;
+}
+
+#openwebrx-panel-js8-message td.message {
+ white-space: nowrap;
+ overflow: hidden;
+ display: flex;
+ flex-direction: row-reverse;
+}
+
+#openwebrx-panel-js8-message .message div {
+ flex: 1;
+}
+
+#openwebrx-panel-js8-message .decimal {
+ text-align: right;
+ width: 35px;
+}
+
+#openwebrx-panel-js8-message .decimal.freq {
+ width: 70px;
+}
+
+#openwebrx-panel-packet-message .message {
+ width: 410px;
+ max-width: 410px;
+}
+
+#openwebrx-panel-packet-message .callsign {
+ width: 80px;
+}
+
+#openwebrx-panel-packet-message .coord {
+ width: 40px;
+ text-align: center;
+}
+
+#openwebrx-panel-pocsag-message .address {
+ width: 100px;
+}
+
+#openwebrx-panel-pocsag-message .message {
+ width: 486px;
+ max-width: 486px;
+ white-space: pre;
+}
+
+.aprs-symbol {
+ display: inline-block;
+ width: 15px;
+ height: 15px;
+ background-size: 240px 90px;
+}
+
+.aprs-symboltable-normal {
+ background-image: url(../../aprs-symbols/aprs-symbols-24-0.png)
+}
+
+.aprs-symboltable-alternate {
+ background-image: url(../../aprs-symbols/aprs-symbols-24-1.png)
+}
+
+.aprs-symboltable-overlay {
+ background-image: url(../../aprs-symbols/aprs-symbols-24-2.png)
+}
+
+.openwebrx-dialog {
+ background-color: #575757;
+ padding: 10px;
+ color: white;
+ position: fixed;
+ font-size: 10pt;
+ border-radius: 15px;
+ -moz-border-radius: 15px;
+ position: fixed;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, 0);
+}
+
+.openwebrx-dialog .form-field {
+ padding: 5px;
+ display: flex;
+ flex-direction: row;
+}
+
+.openwebrx-dialog .form-field:first-child {
+ padding-top: 0;
+}
+
+.openwebrx-dialog label {
+ display: inline-block;
+ flex-grow: 0;
+ width: 70px;
+ padding-right: 20px;
+ margin-top: auto;
+ margin-bottom: auto;
+}
+
+.openwebrx-dialog .form-field input,
+.openwebrx-dialog .form-field select {
+ flex-grow: 1;
+ height: 27px;
+}
+
+.openwebrx-dialog .form-field input {
+ padding: 0 5px;
+}
+
+.openwebrx-dialog .buttons {
+ text-align: right;
+ padding: 5px 5px 0;
+ border-top: 1px solid #666;
+}
+
+.openwebrx-dialog .buttons .openwebrx-button {
+ font-size: 12pt;
+ min-width: 50px;
+ text-align: center;
+ padding: 5px 10px;
+}
+
+#openwebrx-panel-digimodes[data-mode^="bpsk"] #openwebrx-digimode-content-container,
+#openwebrx-panel-digimodes[data-mode^="rtty"] #openwebrx-digimode-content-container,
+#openwebrx-panel-digimodes[data-mode^="bpsk"] #openwebrx-digimode-select-channel,
+#openwebrx-panel-digimodes[data-mode^="rtty"] #openwebrx-digimode-select-channel
+{
+ display: block;
+}
+
+#openwebrx-panel-digimodes[data-mode^="bpsk"] #openwebrx-digimode-canvas-container,
+#openwebrx-panel-digimodes[data-mode^="rtty"] #openwebrx-digimode-canvas-container
+{
+ height: 150px;
+ margin-bottom: 0;
+}
+
+.openwebrx-zoom-button svg {
+ height: 27px;
+}
+
+.openwebrx-slider-button svg {
+ position:relative;
+ top: 1px;
+ height: 14px;
+}
+
+.openwebrx-mute-button svg.muted {
+ display: none;
+}
+
+.openwebrx-mute-button.muted svg.muted {
+ display: initial;
+}
+
+.openwebrx-mute-button.muted svg.unmuted {
+ display: none;
+}
+
+.bookmark .bookmark-actions .openwebrx-button svg {
+ height: 14px;
+}
+
+#openwebrx-waterfall-colors-auto .continuous {
+ display: none;
+}
+
+#openwebrx-waterfall-colors-auto.highlighted .continuous {
+ display: initial;
+}
+
+#openwebrx-waterfall-colors-auto.highlighted .auto {
+ display: none;
+}
+
+.openwebrx-waterfall-container {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+}
+
+.openwebrx-waterfall-container > * {
+ flex: 0 0 auto;
+}
+
+#openwebrx-panel-metadata-wfm {
+ width: 300px;
+ max-height: 300px;
+}
+
+.rds-container {
+ width: 100%;
+ text-align: center;
+ overflow: hidden auto;
+}
+
+.rds-container > *, .rds-radiotext-plus > * {
+ margin: 2px 0;
+}
+
+.rds-container .rds-stationname {
+ font-family: roboto-mono;
+ font-size: 18pt;
+ padding: 10px 0;
+}
+
+.rds-container .rds-stationname,
+.rds-container .rds-identifier,
+.rds-container .rds-prog_type {
+ min-height: 1lh;
+}
+
+.rds-container .rds-radiotext-plus .rds-rtplus-item:not(:empty):before {
+ content: "♫ ";
+}
+
+.rds-container .rds-radiotext-plus .rds-rtplus-programme:not(:empty):before {
+ content: "📅 ";
+}
+
+.rds-container .rds-radiotext-plus ul.rds-rtplus-news {
+ list-style-type: "📰 ";
+ padding-left: 1.5lh;
+}
+
+.rds-container .rds-radiotext-plus .rds-rtplus-weather:not(:empty):before {
+ content: "⛅ ";
+}
+
+.rds-container .rds-radiotext-plus .rds-rtplus-homepage:not(:empty):before {
+ content: "🔗 ";
+}
+
+#openwebrx-panel-metadata-dab {
+ width: 300px;
+}
+
+#openwebrx-panel-metadata-dab .dab-container {
+ width: 100%;
+}
+
+.dab-container > * {
+ margin: 2px 0;
+ text-align: center;
+ overflow: hidden auto;
+}
+
+.dab-container label {
+ display: block;
+ margin: 5px 0;
+}
+
+.dab-container select#dab-service-id {
+ width: 100%;
+ padding: 3px;
+}
+
+.dab-container .dab-ensemble-id:not(:empty):before {
+ content: "Ensemble ID: ";
+}
+
+.dab-container .dab-ensemble-label:not(:empty):before {
+ content: "Ensemble: ";
+}
+
+.under-construction {
+ background-color: #ffd914;
+ color: #111111;
+ text-align: center;
+}
+
+.under-construction h4 {
+ font-size: 15pt;
+}
+
+.under-construction h4 {
+ margin: 0
+}
+
+.under-construction p {
+ margin: 5px 0;
+}
+
+.under-construction-description {
+ max-height: 0;
+ overflow: hidden;
+ transition: max-height .2s ease-in-out;
+}
+
+.under-construction:hover .under-construction-description {
+ max-height: 500px;
+}
diff --git a/htdocs/favicon.ico b/htdocs/favicon.ico
index f8c9a2ae9..6a07f1b1e 100644
Binary files a/htdocs/favicon.ico and b/htdocs/favicon.ico differ
diff --git a/htdocs/features.html b/htdocs/features.html
new file mode 100644
index 000000000..53099b6d0
--- /dev/null
+++ b/htdocs/features.html
@@ -0,0 +1,25 @@
+
+ OpenWebRX Feature report
+
+
+
+
+
+
+
+
+ ${header}
+
+ ${breadcrumb}
+
OpenWebRX Feature Report
+
+
+
Feature
+
Requirement
+
Description
+
Available
+
+
+ ${breadcrumb}
+
+
\ No newline at end of file
diff --git a/htdocs/features.js b/htdocs/features.js
new file mode 100644
index 000000000..0add3b437
--- /dev/null
+++ b/htdocs/features.js
@@ -0,0 +1,23 @@
+$(function(){
+ var converter = new showdown.Converter({openLinksInNewWindow: true});
+ $.ajax('api/features').done(function(data){
+ var $table = $('table.features');
+ $.each(data, function(name, details) {
+ var requirements = $.map(details.requirements, function(r, name){
+ return '
' +
+ '
' +
+ '
' + name + '
' +
+ '
' + converter.makeHtml(r.description) + '
' +
+ '
' + (r.available ? 'YES' : 'NO') + '
' +
+ '
';
+ });
+ $table.append(
+ '
' +
+ '
' + name + '
' +
+ '
' + (details.available ? 'YES' : 'NO') + '
' +
+ '
' +
+ requirements.join("")
+ );
+ })
+ });
+});
diff --git a/htdocs/fonts/RobotoMono-Regular.ttf b/htdocs/fonts/RobotoMono-Regular.ttf
new file mode 100644
index 000000000..7c4ce36a4
Binary files /dev/null and b/htdocs/fonts/RobotoMono-Regular.ttf differ
diff --git a/htdocs/fonts/RobotoMono-Regular.woff b/htdocs/fonts/RobotoMono-Regular.woff
new file mode 100644
index 000000000..1b805eaf2
Binary files /dev/null and b/htdocs/fonts/RobotoMono-Regular.woff differ
diff --git a/htdocs/fonts/RobotoMono-Regular.woff2 b/htdocs/fonts/RobotoMono-Regular.woff2
new file mode 100644
index 000000000..dab25851b
Binary files /dev/null and b/htdocs/fonts/RobotoMono-Regular.woff2 differ
diff --git a/htdocs/gfx/favicon128.png b/htdocs/gfx/favicon128.png
new file mode 100644
index 000000000..ad42441f1
Binary files /dev/null and b/htdocs/gfx/favicon128.png differ
diff --git a/htdocs/gfx/favicon32.png b/htdocs/gfx/favicon32.png
new file mode 100644
index 000000000..2c534af55
Binary files /dev/null and b/htdocs/gfx/favicon32.png differ
diff --git a/htdocs/gfx/favicon44.png b/htdocs/gfx/favicon44.png
new file mode 100644
index 000000000..d21f326e7
Binary files /dev/null and b/htdocs/gfx/favicon44.png differ
diff --git a/htdocs/gfx/favicon64.png b/htdocs/gfx/favicon64.png
new file mode 100644
index 000000000..b08e03fc7
Binary files /dev/null and b/htdocs/gfx/favicon64.png differ
diff --git a/htdocs/gfx/favicon96.png b/htdocs/gfx/favicon96.png
new file mode 100644
index 000000000..ee9412988
Binary files /dev/null and b/htdocs/gfx/favicon96.png differ
diff --git a/htdocs/gfx/font-expletus-sans/ExpletusSans-Medium.ttf b/htdocs/gfx/font-expletus-sans/ExpletusSans-Medium.ttf
deleted file mode 100644
index dfc87f871..000000000
Binary files a/htdocs/gfx/font-expletus-sans/ExpletusSans-Medium.ttf and /dev/null differ
diff --git a/htdocs/gfx/font-expletus-sans/OFL.txt b/htdocs/gfx/font-expletus-sans/OFL.txt
deleted file mode 100644
index 5979654ee..000000000
--- a/htdocs/gfx/font-expletus-sans/OFL.txt
+++ /dev/null
@@ -1,93 +0,0 @@
-Copyright (c) 2011, Jasper de Waard (jasper@designtown.nl),
-with Reserved Font Name "Expletus Sans".
-This Font Software is licensed under the SIL Open Font License, Version 1.1.
-This license is copied below, and is also available with a FAQ at:
-http://scripts.sil.org/OFL
-
-
------------------------------------------------------------
-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
------------------------------------------------------------
-
-PREAMBLE
-The goals of the Open Font License (OFL) are to stimulate worldwide
-development of collaborative font projects, to support the font creation
-efforts of academic and linguistic communities, and to provide a free and
-open framework in which fonts may be shared and improved in partnership
-with others.
-
-The OFL allows the licensed fonts to be used, studied, modified and
-redistributed freely as long as they are not sold by themselves. The
-fonts, including any derivative works, can be bundled, embedded,
-redistributed and/or sold with any software provided that any reserved
-names are not used by derivative works. The fonts and derivatives,
-however, cannot be released under any other type of license. The
-requirement for fonts to remain under this license does not apply
-to any document created using the fonts or their derivatives.
-
-DEFINITIONS
-"Font Software" refers to the set of files released by the Copyright
-Holder(s) under this license and clearly marked as such. This may
-include source files, build scripts and documentation.
-
-"Reserved Font Name" refers to any names specified as such after the
-copyright statement(s).
-
-"Original Version" refers to the collection of Font Software components as
-distributed by the Copyright Holder(s).
-
-"Modified Version" refers to any derivative made by adding to, deleting,
-or substituting -- in part or in whole -- any of the components of the
-Original Version, by changing formats or by porting the Font Software to a
-new environment.
-
-"Author" refers to any designer, engineer, programmer, technical
-writer or other person who contributed to the Font Software.
-
-PERMISSION & CONDITIONS
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of the Font Software, to use, study, copy, merge, embed, modify,
-redistribute, and sell modified and unmodified copies of the Font
-Software, subject to the following conditions:
-
-1) Neither the Font Software nor any of its individual components,
-in Original or Modified Versions, may be sold by itself.
-
-2) Original or Modified Versions of the Font Software may be bundled,
-redistributed and/or sold with any software, provided that each copy
-contains the above copyright notice and this license. These can be
-included either as stand-alone text files, human-readable headers or
-in the appropriate machine-readable metadata fields within text or
-binary files as long as those fields can be easily viewed by the user.
-
-3) No Modified Version of the Font Software may use the Reserved Font
-Name(s) unless explicit written permission is granted by the corresponding
-Copyright Holder. This restriction only applies to the primary font name as
-presented to the users.
-
-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
-Software shall not be used to promote, endorse or advertise any
-Modified Version, except to acknowledge the contribution(s) of the
-Copyright Holder(s) and the Author(s) or with their explicit written
-permission.
-
-5) The Font Software, modified or unmodified, in part or in whole,
-must be distributed entirely under this license, and must not be
-distributed under any other license. The requirement for fonts to
-remain under this license does not apply to any document created
-using the Font Software.
-
-TERMINATION
-This license becomes null and void if any of the above conditions are
-not met.
-
-DISCLAIMER
-THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
-OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/htdocs/gfx/openwebrx-3d-spectrum.png b/htdocs/gfx/openwebrx-3d-spectrum.png
deleted file mode 100644
index 06ad49502..000000000
Binary files a/htdocs/gfx/openwebrx-3d-spectrum.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-avatar-background.png b/htdocs/gfx/openwebrx-avatar-background.png
deleted file mode 100644
index e52cb0b95..000000000
Binary files a/htdocs/gfx/openwebrx-avatar-background.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-avatar.png b/htdocs/gfx/openwebrx-avatar.png
index 7e9736fdb..fc2052912 100644
Binary files a/htdocs/gfx/openwebrx-avatar.png and b/htdocs/gfx/openwebrx-avatar.png differ
diff --git a/htdocs/gfx/openwebrx-background-cool-blue.png b/htdocs/gfx/openwebrx-background-cool-blue.png
index 7430bd8a8..236b366b3 100644
Binary files a/htdocs/gfx/openwebrx-background-cool-blue.png and b/htdocs/gfx/openwebrx-background-cool-blue.png differ
diff --git a/htdocs/gfx/openwebrx-background-cool-blue.webp b/htdocs/gfx/openwebrx-background-cool-blue.webp
new file mode 100644
index 000000000..51f7852bf
Binary files /dev/null and b/htdocs/gfx/openwebrx-background-cool-blue.webp differ
diff --git a/htdocs/gfx/openwebrx-background-lingrad.png b/htdocs/gfx/openwebrx-background-lingrad.png
deleted file mode 100644
index 48537f7a0..000000000
Binary files a/htdocs/gfx/openwebrx-background-lingrad.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-directcall.svg b/htdocs/gfx/openwebrx-directcall.svg
new file mode 100644
index 000000000..344011213
--- /dev/null
+++ b/htdocs/gfx/openwebrx-directcall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/htdocs/gfx/openwebrx-groupcall.svg b/htdocs/gfx/openwebrx-groupcall.svg
new file mode 100644
index 000000000..5083a5763
--- /dev/null
+++ b/htdocs/gfx/openwebrx-groupcall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/htdocs/gfx/openwebrx-ha5kfu-top-logo.png b/htdocs/gfx/openwebrx-ha5kfu-top-logo.png
deleted file mode 100644
index 2686eef0a..000000000
Binary files a/htdocs/gfx/openwebrx-ha5kfu-top-logo.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-logo-big.png b/htdocs/gfx/openwebrx-logo-big.png
deleted file mode 100644
index dcafb2ee3..000000000
Binary files a/htdocs/gfx/openwebrx-logo-big.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-panel-log.png b/htdocs/gfx/openwebrx-panel-log.png
deleted file mode 100644
index 58e6fd5f0..000000000
Binary files a/htdocs/gfx/openwebrx-panel-log.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-panel-receiver.png b/htdocs/gfx/openwebrx-panel-receiver.png
deleted file mode 100644
index 5c80c3b64..000000000
Binary files a/htdocs/gfx/openwebrx-panel-receiver.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-panel-status.png b/htdocs/gfx/openwebrx-panel-status.png
deleted file mode 100644
index 064b54f71..000000000
Binary files a/htdocs/gfx/openwebrx-panel-status.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-play-button.png b/htdocs/gfx/openwebrx-play-button.png
deleted file mode 100644
index 4a0652178..000000000
Binary files a/htdocs/gfx/openwebrx-play-button.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-rx-details-arrow-up.png b/htdocs/gfx/openwebrx-rx-details-arrow-up.png
deleted file mode 100644
index 0baccd041..000000000
Binary files a/htdocs/gfx/openwebrx-rx-details-arrow-up.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-rx-details-arrow.png b/htdocs/gfx/openwebrx-rx-details-arrow.png
deleted file mode 100644
index 9995118f0..000000000
Binary files a/htdocs/gfx/openwebrx-rx-details-arrow.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-scale-background.png b/htdocs/gfx/openwebrx-scale-background.png
index 7fbb4d249..91453c589 100644
Binary files a/htdocs/gfx/openwebrx-scale-background.png and b/htdocs/gfx/openwebrx-scale-background.png differ
diff --git a/htdocs/gfx/openwebrx-speaker-muted.png b/htdocs/gfx/openwebrx-speaker-muted.png
deleted file mode 100644
index 0d5457039..000000000
Binary files a/htdocs/gfx/openwebrx-speaker-muted.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-speaker.png b/htdocs/gfx/openwebrx-speaker.png
deleted file mode 100644
index 6c88e23c4..000000000
Binary files a/htdocs/gfx/openwebrx-speaker.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-squelch-button.png b/htdocs/gfx/openwebrx-squelch-button.png
deleted file mode 100644
index f67177c5a..000000000
Binary files a/htdocs/gfx/openwebrx-squelch-button.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-top-logo.png b/htdocs/gfx/openwebrx-top-logo.png
deleted file mode 100644
index 477242524..000000000
Binary files a/htdocs/gfx/openwebrx-top-logo.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-top-photo.jpg b/htdocs/gfx/openwebrx-top-photo.jpg
index cf521c752..afc8e7e02 100644
Binary files a/htdocs/gfx/openwebrx-top-photo.jpg and b/htdocs/gfx/openwebrx-top-photo.jpg differ
diff --git a/htdocs/gfx/openwebrx-waterfall-auto.png b/htdocs/gfx/openwebrx-waterfall-auto.png
deleted file mode 100644
index 7e41302ac..000000000
Binary files a/htdocs/gfx/openwebrx-waterfall-auto.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-waterfall-default.png b/htdocs/gfx/openwebrx-waterfall-default.png
deleted file mode 100644
index 1cd39fa59..000000000
Binary files a/htdocs/gfx/openwebrx-waterfall-default.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-zoom-in-total.png b/htdocs/gfx/openwebrx-zoom-in-total.png
deleted file mode 100644
index 3646a377a..000000000
Binary files a/htdocs/gfx/openwebrx-zoom-in-total.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-zoom-in.png b/htdocs/gfx/openwebrx-zoom-in.png
deleted file mode 100644
index c8df0c8ac..000000000
Binary files a/htdocs/gfx/openwebrx-zoom-in.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-zoom-out-total.png b/htdocs/gfx/openwebrx-zoom-out-total.png
deleted file mode 100644
index d83b61df4..000000000
Binary files a/htdocs/gfx/openwebrx-zoom-out-total.png and /dev/null differ
diff --git a/htdocs/gfx/openwebrx-zoom-out.png b/htdocs/gfx/openwebrx-zoom-out.png
deleted file mode 100644
index 60cd91209..000000000
Binary files a/htdocs/gfx/openwebrx-zoom-out.png and /dev/null differ
diff --git a/htdocs/gfx/svg-defs.svg b/htdocs/gfx/svg-defs.svg
new file mode 100644
index 000000000..251b05170
--- /dev/null
+++ b/htdocs/gfx/svg-defs.svg
@@ -0,0 +1,28 @@
+
\ No newline at end of file
diff --git a/htdocs/inactive.html b/htdocs/inactive.html
deleted file mode 100644
index c7214c5c8..000000000
--- a/htdocs/inactive.html
+++ /dev/null
@@ -1,85 +0,0 @@
-
-
-OpenWebRX
-
-
-
-
-
-
-
- Sorry, the receiver is inactive due to internal error.
-