From 81f42273cb583d5cb655b510bef7fc597033f517 Mon Sep 17 00:00:00 2001 From: geneva-miller <45578681+geneva-miller@users.noreply.github.com> Date: Mon, 11 May 2020 20:40:22 -0700 Subject: [PATCH 01/10] Remove .DS_Store (#167) * remove .DS_Store from desktop folder * add OS generated files should be in root .gitignore --- .gitignore | 7 +++++++ desktop/.DS_Store | Bin 6148 -> 0 bytes 2 files changed, 7 insertions(+) delete mode 100644 desktop/.DS_Store diff --git a/.gitignore b/.gitignore index c023183de..9428aa02e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,14 @@ __pycache__/ # C extensions *.so +# OS generated files .DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db # Distribution / packaging .Python diff --git a/desktop/.DS_Store b/desktop/.DS_Store deleted file mode 100644 index b3c57750e9b73f1760461f8d4074c2e8fcefdeec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKT}s3-5T4N$3cl>4kGaC$AT0F+djYjo_o1bQb-~9RYj3b;@EG2}Z!)8_G$8mO zA~TSDlgv+&51Mp{h_}afL$oHM3QdqjDTquLO&8{T1>~G#q>dix&>otukmxT?$=)mM z`IYXer^)^whM~iumd5URvulQaoSbHnpdaeGJ-#$VZ~OPt)47zVAKGty#{FWg$+E6G z1I~am;0!neKVd+gH%N7@=)E)G3^)T{49NWu&;+AlQp{Hex?BPPSD4NMU1|x535L-y zDZ&C_4Fzf_TZzFMj`?7I(J(1$II$HUY%6~jFPv9L{*c{?qoViDfHN>>;8KTcdH-+m z$rOwHd5Djk0cYT!F~F;OS8s7Bds`RBleadY-JywyUlIiZefAT8f!rfE*{Jp)I{czx WQj{z*pTmLvBajL4&KdXx20j4Xt2U7U From cf91cb4f284561c4a48fe801adec18ac11c4b891 Mon Sep 17 00:00:00 2001 From: ccamacho89 <62677090+ccamacho89@users.noreply.github.com> Date: Mon, 1 Jun 2020 16:44:54 -0700 Subject: [PATCH 02/10] Update readme with correct path for mounting docker container (#172) Minimizes risk of being in incorrect working directory when running new docker image. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 55e9dcdb9..e17362560 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ docker build -t caliban . ```bash docker run \ -p 5900:5900 \ --v $PWD/caliban/desktop:/usr/src/app/desktop \ +-v $PWD/desktop:/usr/src/app/desktop \ --privileged \ caliban:latest ``` From fc856c75ddf8da4647621487820ed233de433d7c Mon Sep 17 00:00:00 2001 From: willgraf <7930703+willgraf@users.noreply.github.com> Date: Wed, 22 Jul 2020 11:23:03 -0700 Subject: [PATCH 03/10] Merge browser module into master. (#187) * Browser/fix/open empty annotations (#111) * Browser/feature/disable page scroll (#116) * Browser/feature/add canvas padding (#117) * Update, refactor brush (#120) * Update browser contrast adjustment (#121) * Add outline capability to browser (#122) * Initial timestamp tracking (#124) * Initial url options, RGB mode (#130) * Update initial brush params (#136) * Initial changes to image modification code (#138) * Fix npz upload issues (#139) * Misc small changes to rgb (#141) * Add travis and use Flask Blueprints to remove redundant application files. (#144) * Add docker-compose.yml for easier local development (#146) * Change environment variables to AWS standards. (#147) * fix S3 env vars that got lost in the merge. (#148) * Update npz canvas (expansion, better scaling, pan+zoom) (#149) * Update HTML templates with the materialize CSS framework (#154) * Add logger statements to most backend routes. (#157) * Use distutils.util.strtobool to cast boolean request args to bools. (#163) * Fix some browser rgb image generation edge cases (#164) * Use PIL to create PNGs if not using a cmap. (#165) * Don't need to return raw data on action requests. (#166) * Save npzs with X and y (#168) * Hide labels, bugfixes (#174) * add flask-cors and enable CORS for all routes. (#178) * Update browser with better logging and error handling. (#179) * Add unit tests for Flask application. (#186) Co-authored-by: geneva-miller <45578681+geneva-miller@users.noreply.github.com> Co-authored-by: Thomas Dougherty --- .coveragerc | 29 + .travis.yml | 23 + browser/.ebextensions/healthcheckurl.config | 4 + browser/.ebextensions/options.config | 3 +- browser/.env.example | 11 + browser/Dockerfile | 13 + browser/README.md | 32 +- browser/application.py | 379 +----- browser/blueprints.py | 336 +++++ browser/blueprints_test.py | 176 +++ browser/caliban.py | 695 ++++++---- browser/config.py | 35 +- browser/conftest.py | 42 + browser/docker-compose.yaml | 25 + browser/eb_application.py | 357 ----- browser/env.example | 10 - browser/helpers.py | 2 + browser/helpers_test.py | 42 + browser/imgutils.py | 21 +- browser/imgutils_test.py | 42 + browser/models.py | 85 ++ browser/models_test.py | 49 + browser/requirements-test.txt | 9 + browser/requirements.txt | 38 +- browser/static/css/footer.css | 3 - browser/static/css/form.css | 54 - browser/static/css/infopane.css | 23 - browser/static/css/main.css | 133 +- browser/static/css/materialize.min.css | 13 + browser/static/css/navigation.css | 45 - browser/static/css/normalize.css | 359 ++++- browser/static/favicon.ico | Bin 0 -> 15086 bytes browser/static/js/adjust.js | 277 ++++ browser/static/js/brush.js | 257 ++++ browser/static/js/infopane.js | 14 - browser/static/js/main_track.js | 319 ++--- browser/static/js/main_zstack.js | 1151 +++++++++++------ browser/static/js/materialize.min.js | 6 + browser/templates/base.html | 75 ++ browser/templates/footer.html | 3 - browser/templates/form.html | 69 - browser/templates/index.html | 76 ++ browser/templates/index_track.html | 131 -- browser/templates/index_zstack.html | 79 -- browser/templates/infopane.html | 103 -- browser/templates/navigation.html | 7 - browser/templates/partials/infopane.html | 126 ++ .../templates/partials/infopane_abridged.html | 89 ++ browser/templates/partials/loading-bar.html | 40 + browser/templates/partials/track_table.html | 58 + browser/templates/partials/zstack_table.html | 23 + browser/templates/tool.html | 85 ++ pytest.ini | 25 + 53 files changed, 3904 insertions(+), 2197 deletions(-) create mode 100644 .coveragerc create mode 100644 .travis.yml create mode 100644 browser/.ebextensions/healthcheckurl.config create mode 100644 browser/.env.example create mode 100644 browser/Dockerfile create mode 100644 browser/blueprints.py create mode 100644 browser/blueprints_test.py create mode 100644 browser/conftest.py create mode 100644 browser/docker-compose.yaml delete mode 100644 browser/eb_application.py delete mode 100644 browser/env.example create mode 100644 browser/helpers_test.py create mode 100644 browser/imgutils_test.py create mode 100644 browser/models.py create mode 100644 browser/models_test.py create mode 100644 browser/requirements-test.txt delete mode 100644 browser/static/css/footer.css delete mode 100644 browser/static/css/form.css delete mode 100644 browser/static/css/infopane.css create mode 100644 browser/static/css/materialize.min.css delete mode 100644 browser/static/css/navigation.css create mode 100644 browser/static/favicon.ico create mode 100644 browser/static/js/adjust.js create mode 100644 browser/static/js/brush.js delete mode 100644 browser/static/js/infopane.js create mode 100644 browser/static/js/materialize.min.js create mode 100644 browser/templates/base.html delete mode 100644 browser/templates/footer.html delete mode 100644 browser/templates/form.html create mode 100644 browser/templates/index.html delete mode 100644 browser/templates/index_track.html delete mode 100644 browser/templates/index_zstack.html delete mode 100644 browser/templates/infopane.html delete mode 100644 browser/templates/navigation.html create mode 100644 browser/templates/partials/infopane.html create mode 100644 browser/templates/partials/infopane_abridged.html create mode 100644 browser/templates/partials/loading-bar.html create mode 100644 browser/templates/partials/track_table.html create mode 100644 browser/templates/partials/zstack_table.html create mode 100644 browser/templates/tool.html create mode 100644 pytest.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..fb610bd63 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,29 @@ +# .coveragerc to control coverage.py +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + except ImportError + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + +ignore_errors = True +# fail_under = 70 +show_missing = True + +omit = + **/*_test.py + env/**/*.py + venv/**/*.py + ENV/**/*.py + VENV/**/*.py + desktop/env/**/*.py + desktop/venv/**/*.py + desktop/ENV/**/*.py + desktop/VENV/**/*.py + browser/env/**/*.py + browser/venv/**/*.py + browser/ENV/**/*.py + browser/VENV/**/*.py + browser/conftest.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..a380d10d5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +dist: xenial + +git: + depth: false + +language: python + +cache: pip + +python: + - 3.6 + - 3.7 + - 3.8 + +install: + - pip install -r browser/requirements.txt + - pip install -r browser/requirements-test.txt + +script: + - pytest browser --cov browser --pep8 + +after_success: + - coveralls diff --git a/browser/.ebextensions/healthcheckurl.config b/browser/.ebextensions/healthcheckurl.config new file mode 100644 index 000000000..351dc2461 --- /dev/null +++ b/browser/.ebextensions/healthcheckurl.config @@ -0,0 +1,4 @@ +option_settings: + - namespace: aws:elasticbeanstalk:application + option_name: Application Healthcheck URL + value: /health diff --git a/browser/.ebextensions/options.config b/browser/.ebextensions/options.config index dcd000a05..71203417f 100644 --- a/browser/.ebextensions/options.config +++ b/browser/.ebextensions/options.config @@ -1,5 +1,4 @@ option_settings: - namespace: aws:elasticbeanstalk:container:python option_name: WSGIPath - value: eb_application.py - + value: application.py diff --git a/browser/.env.example b/browser/.env.example new file mode 100644 index 000000000..2b560721a --- /dev/null +++ b/browser/.env.example @@ -0,0 +1,11 @@ +# Flask settings +DEBUG= +TEMPLATES_AUTO_RELOAD= + +# AWS credentials +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= + +# SQL Alchemy settings +SQLALCHEMY_DATABASE_URI= +SQLALCHEMY_TRACK_MODIFICATIONS= diff --git a/browser/Dockerfile b/browser/Dockerfile new file mode 100644 index 000000000..3f32e06c7 --- /dev/null +++ b/browser/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.7 + +WORKDIR /usr/src/app + +COPY requirements.txt requirements.txt + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 5000 + +CMD ["/bin/sh", "-c", "python application.py"] diff --git a/browser/README.md b/browser/README.md index dec6fd44c..7e1e5f636 100644 --- a/browser/README.md +++ b/browser/README.md @@ -18,6 +18,32 @@ pip install -r requirements.txt ```bash python3 application.py ``` +## To use docker-compose for local development +Add your AWS credentials to ```docker-compose.yaml```. + +From the ```caliban/browser``` folder, run: +```bash +sudo docker-compose up --build -d +``` +Wait a minute for the database to finish setting up before running: +``` +sudo docker-compose restart app +``` +You can now go to 0.0.0.0:5000 in a browser window to access the local version of the tool. + +To interact with the local mysql database: +``` +sudo docker exec -it browser_db_1 bash +mysql -p +``` +When finished: +``` +sudo docker-compose down +``` +(optional) +``` +sudo docker system prune --volumes +``` ## Structure of Browser Version @@ -81,7 +107,7 @@ Keybinds in pixel editing mode are different from those in the label-editing mod Annotation mode focuses on using an adjustable brush to modify annotations on a pixel level, rather than using operations that apply to every instance of a label within a frame or set of frames. The brush tool will only make changes to the currently selected value. Ie, a brush set to edit cell 5 will only add or erase "5" to the annotated image. -*-/=* - increment value that brush is applying +*[ (left bracket) / ] (right bracket)* - increment value that brush is applying *↓ ↑* - change size of brush tool @@ -100,6 +126,10 @@ Annotation mode focuses on using an adjustable brush to modify annotations on a ### Viewing Options: +*spacebar + click and drag* - pan across canvas + +*-/= keys or alt + scroll wheel* - zoom in and out + *c* - cycle between different channels when no cells are selected *e* - toggle annotation mode (when nothing else selected) diff --git a/browser/application.py b/browser/application.py index 1aed32f9f..2b9a71a3d 100644 --- a/browser/application.py +++ b/browser/application.py @@ -1,348 +1,93 @@ -"""Flask app route handlers""" +"""Flask application entrypoint""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function -import base64 -import json -import os -import pickle -import re -import sqlite3 -import sys -import traceback +import logging -from flask import Flask, jsonify, render_template, request, redirect +from flask import Flask +from flask.logging import default_handler +from flask_cors import CORS -from helpers import is_trk_file, is_npz_file -from caliban import TrackReview, ZStackReview +from flask_compress import Compress -# Create and configure the app -app = Flask(__name__) # pylint: disable=C0103 -app.config.from_object("config") +import config +from blueprints import bp +from models import db -@app.route("/upload_file/", methods=["GET", "POST"]) -def upload_file(project_id): - ''' Upload .trk/.npz data file to AWS S3 bucket. - ''' - conn = create_connection("caliban.db") - # Use id to grab appropriate TrackReview/ZStackReview object from database - id_exists = get_project(conn, project_id) +compress = Compress() # pylint: disable=C0103 - if id_exists is None: - conn.close() - return jsonify({'error': 'project_id not found'}), 404 - state = pickle.loads(id_exists[2]) +class ReverseProxied(object): + """Enable TLS for internal requests. - # Call function in caliban.py to save data file and send to S3 bucket - if is_trk_file(id_exists[1]): - state.action_save_track() - elif is_npz_file(id_exists[1]): - state.action_save_zstack() - - # Delete id and object from database - delete_project(conn, project_id) - conn.close() - - return redirect("/") - - -@app.route("/action///", methods=["POST"]) -def action(project_id, action_type, frame): - ''' Make an edit operation to the data file and update the object - in the database. - ''' - # obtain 'info' parameter data sent by .js script - info = {k: json.loads(v) for k, v in request.values.to_dict().items()} - frame = int(frame) - - try: - conn = create_connection("caliban.db") - # Use id to grab appropriate TrackReview/ZStackReview object from database - id_exists = get_project(conn, project_id) - - if id_exists is None: - conn.close() - return jsonify({'error': 'project_id not found'}), 404 - - state = pickle.loads(id_exists[2]) - # Perform edit operation on the data file - state.action(action_type, info) - frames_changed = state.frames_changed - info_changed = state.info_changed - - state.frames_changed = state.info_changed = False - - # Update object in local database - update_object(conn, (id_exists[1], state, project_id)) - conn.close() - - except Exception as e: - traceback.print_exc() - return jsonify({"error": str(e)}), 500 - - if info_changed: - tracks = state.readable_tracks - else: - tracks = False - - if frames_changed: - img = state.get_frame(frame, raw=False) - raw = state.get_frame(frame, raw=True) - edit_arr = state.get_array(frame) - - encode = lambda x: base64.encodebytes(x.read()).decode() - - img_payload = { - 'raw': f'data:image/png;base64,{encode(raw)}', - 'segmented': f'data:image/png;base64,{encode(img)}', - 'seg_arr': edit_arr.tolist() - } - else: - img_payload = False - - return jsonify({"tracks": tracks, "imgs": img_payload}) - -@app.route("/frame//") -def get_frame(frame, project_id): - ''' Serve modes of frames as pngs. Send pngs and color mappings of - cells to .js file. - ''' - frame = int(frame) - conn = create_connection("caliban.db") - # Use id to grab appropriate TrackReview/ZStackReview object from database - id_exists = get_project(conn, project_id) - conn.close() - - if id_exists is None: - return jsonify({'error': 'project_id not found'}), 404 - - state = pickle.loads(id_exists[2]) - - # Obtain raw, mask, and edit mode frames - img = state.get_frame(frame, raw=False) - raw = state.get_frame(frame, raw=True) - - # Obtain color map of the cells - edit_arr = state.get_array(frame) - - encode = lambda x: base64.encodebytes(x.read()).decode() - - payload = { - 'raw': f'data:image/png;base64,{encode(raw)}', - 'segmented': f'data:image/png;base64,{encode(img)}', - 'seg_arr': edit_arr.tolist() - } - - return jsonify(payload) - - -@app.route("/load/", methods=["POST"]) -def load(filename): - ''' Initate TrackReview/ZStackReview object and load object to database. - Send specific attributes of the object to the .js file. - ''' - conn = create_connection("caliban.db") - - print(f"Loading track at {filename}", file=sys.stderr) - - folders = re.split('__', filename) - filename = folders[len(folders) - 1] - subfolders = folders[2:len(folders)] - - subfolders = '/'.join(subfolders) - - input_bucket = folders[0] - output_bucket = folders[1] - - if is_trk_file(filename): - # Initate TrackReview object and entry in database - track_review = TrackReview(filename, input_bucket, output_bucket, subfolders) - project_id = create_project(conn, filename, track_review) - conn.commit() - conn.close() - - # Send attributes to .js file - return jsonify({ - "max_frames": track_review.max_frames, - "tracks": track_review.readable_tracks, - "dimensions": track_review.dimensions, - "project_id": project_id, - "screen_scale": track_review.scale_factor - }) - - if is_npz_file(filename): - # Initate ZStackReview object and entry in database - zstack_review = ZStackReview(filename, input_bucket, output_bucket, subfolders) - project_id = create_project(conn, filename, zstack_review) - conn.commit() - conn.close() - - # Send attributes to .js file - return jsonify({ - "max_frames": zstack_review.max_frames, - "channel_max": zstack_review.channel_max, - "feature_max": zstack_review.feature_max, - "tracks": zstack_review.readable_tracks, - "dimensions": zstack_review.dimensions, - "project_id": project_id, - "screen_scale": zstack_review.scale_factor - }) - - conn.close() - error = { - 'error': 'invalid file extension: {}'.format( - os.path.splitext(filename)[-1]) - } - return jsonify(error), 400 - - -@app.route('/', methods=['GET', 'POST']) -def form(): - ''' Request HTML landing page to be rendered if user requests for - http://127.0.0.1:5000/. - ''' - return render_template('form.html') - - -@app.route('/tool', methods=['GET', 'POST']) -def tool(): - ''' Request HTML caliban tool page to be rendered after user inputs - filename in the landing page. - ''' - filename = request.form['filename'] - print(f"{filename} is filename", file=sys.stderr) - - new_filename = 'caliban-input__caliban-output__test__{}'.format( - str(filename)) - - if is_trk_file(new_filename): - return render_template('index_track.html', filename=new_filename) - if is_npz_file(new_filename): - return render_template('index_zstack.html', filename=new_filename) - - error = { - 'error': 'invalid file extension: {}'.format( - os.path.splitext(filename)[-1]) - } - return jsonify(error), 400 - - -@app.route('/', methods=['GET', 'POST']) -def shortcut(filename): - ''' Request HTML caliban tool page to be rendered if user makes a URL - request to access a specific data file that has been preloaded to the - input S3 bucket (ex. http://127.0.0.1:5000/test.npz). - ''' - - if is_trk_file(filename): - return render_template('index_track.html', filename=filename) - if is_npz_file(filename): - return render_template('index_zstack.html', filename=filename) - - error = { - 'error': 'invalid file extension: {}'.format( - os.path.splitext(filename)[-1]) - } - return jsonify(error), 400 + Found in: https://stackoverflow.com/questions/30743696 + """ + def __init__(self, app): + self.app = app + def __call__(self, environ, start_response): + scheme = environ.get('HTTP_X_FORWARDED_PROTO') + if scheme: + environ['wsgi.url_scheme'] = scheme + return self.app(environ, start_response) -def create_connection(db_file): - ''' Create a database connection to a SQLite database. - ''' - conn = None - try: - conn = sqlite3.connect(db_file) - except sqlite3.Error as err: - print(err) - return conn +def initialize_logger(): + """Set up logger format and level""" + formatter = logging.Formatter( + '[%(asctime)s]:[%(levelname)s]:[%(name)s]: %(message)s') -def create_table(conn, create_table_sql): - ''' Create a table from the create_table_sql statement. - ''' - try: - cursor = conn.cursor() - cursor.execute(create_table_sql) - except sqlite3.Error as err: - print(err) + default_handler.setFormatter(formatter) + default_handler.setLevel(logging.DEBUG) + wsgi_handler = logging.StreamHandler( + stream='ext://flask.logging.wsgi_errors_stream') + wsgi_handler.setFormatter(formatter) + wsgi_handler.setLevel(logging.DEBUG) -def create_project(conn, filename, data): - ''' Create a new project in the database table. - ''' - sql = ''' INSERT INTO projects(filename, state) - VALUES(?, ?) ''' - cursor = conn.cursor() + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + logger.addHandler(default_handler) - # convert object to binary data to be stored as data type BLOB - state_data = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) + # 3rd party loggers + logging.getLogger('sqlalchemy').addHandler(logging.DEBUG) + logging.getLogger('botocore').setLevel(logging.INFO) + logging.getLogger('urllib3').setLevel(logging.INFO) - cursor.execute(sql, (filename, sqlite3.Binary(state_data))) - return cursor.lastrowid +def create_app(**config_overrides): + """Factory to create the Flask application""" + app = Flask(__name__) -def update_object(conn, project): - ''' Update filename, state of a project. - ''' - sql = ''' UPDATE projects - SET filename = ? , - state = ? - WHERE id = ?''' + CORS(app) - # convert object to binary data to be stored as data type BLOB - state_data = pickle.dumps(project[1], pickle.HIGHEST_PROTOCOL) + app.config.from_object(config) + # apply overrides + app.config.update(config_overrides) - cur = conn.cursor() - cur.execute(sql, (project[0], sqlite3.Binary(state_data), project[2])) - conn.commit() + app.wsgi_app = ReverseProxied(app.wsgi_app) + app.jinja_env.auto_reload = True -def get_project(conn, project_id): - '''Fetches TrackReview/ZStackReview object from database by project_id. + db.app = app # setting context + db.init_app(app) - Args: - conn (obj): SQL database connection. - project_id (int): The primary key of the projects table. + db.create_all() - Returns: - tuple: all data columns matching the project_id. - ''' - cur = conn.cursor() - cur.execute("SELECT * FROM {tn} WHERE {idf}={my_id}".format( - tn="projects", - idf="id", - my_id=project_id - )) - return cur.fetchone() + app.register_blueprint(bp) + compress.init_app(app) -def delete_project(conn, project_id): - ''' Delete data object (TrackReview/ZStackReview) by id. - ''' - sql = 'DELETE FROM projects WHERE id=?' - cur = conn.cursor() - cur.execute(sql, (project_id,)) - conn.commit() + return app -def main(): - ''' Runs app and initiates database file if it doesn't exist. - ''' - conn = create_connection("caliban.db") - sql_create_projects_table = """ - CREATE TABLE IF NOT EXISTS projects ( - id integer PRIMARY KEY, - filename text NOT NULL, - state blob NOT NULL - ); - """ - create_table(conn, sql_create_projects_table) - conn.commit() - conn.close() +application = create_app() # pylint: disable=C0103 - app.jinja_env.auto_reload = True - app.config['TEMPLATES_AUTO_RELOAD'] = True - app.run('0.0.0.0', port=5000) -if __name__ == "__main__": - main() +if __name__ == '__main__': + initialize_logger() + application.run('0.0.0.0', + port=application.config['PORT'], + debug=application.config['DEBUG']) diff --git a/browser/blueprints.py b/browser/blueprints.py new file mode 100644 index 000000000..e9ac11da8 --- /dev/null +++ b/browser/blueprints.py @@ -0,0 +1,336 @@ +"""Flask blueprint for modular routes.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import base64 +import distutils +import distutils.util +import json +import os +import pickle +import re +import timeit +import traceback + +from flask import Blueprint +from flask import jsonify +from flask import render_template +from flask import request +from flask import redirect +from flask import current_app +from werkzeug.exceptions import HTTPException + +from helpers import is_trk_file, is_npz_file +from caliban import TrackReview, ZStackReview +from models import Project + + +bp = Blueprint('caliban', __name__) # pylint: disable=C0103 + + +def load_project_state(project): + """Unpickle the project's state into a Caliban object""" + start = timeit.default_timer() + state = pickle.loads(project.state) + current_app.logger.debug('Unpickled project "%s" state in %s s.', + project.id, timeit.default_timer() - start) + return state + + +@bp.route('/health') +def health(): + """Returns success if the application is ready.""" + return jsonify({'message': 'success'}), 200 + + +@bp.errorhandler(Exception) +def handle_exception(error): + """Handle all uncaught exceptions""" + # pass through HTTP errors + if isinstance(error, HTTPException): + return error + + current_app.logger.error('Encountered %s: %s', + error.__class__.__name__, error) + + # now you're handling non-HTTP exceptions only + return jsonify({'message': str(error)}), 500 + + +@bp.route('/upload_file/', methods=['GET', 'POST']) +def upload_file(project_id): + '''Upload .trk/.npz data file to AWS S3 bucket.''' + start = timeit.default_timer() + # Use id to grab appropriate TrackReview/ZStackReview object from database + project = Project.get_project_by_id(project_id) + + if not project: + return jsonify({'error': 'project_id not found'}), 404 + + state = load_project_state(project) + + # Call function in caliban.py to save data file and send to S3 bucket + if is_trk_file(project.filename): + state.action_save_track() + elif is_npz_file(project.filename): + state.action_save_zstack() + + # add "finished" timestamp and null out state longblob + Project.finish_project(project) + + current_app.logger.debug('Uploaded file "%s" for project "%s" in %s s.', + project.filename, project_id, + timeit.default_timer() - start) + + return redirect('/') + + +@bp.route('/action///', methods=['POST']) +def action(project_id, action_type, frame): + ''' Make an edit operation to the data file and update the object + in the database. + ''' + start = timeit.default_timer() + # obtain 'info' parameter data sent by .js script + info = {k: json.loads(v) for k, v in request.values.to_dict().items()} + + try: + # Use id to grab appropriate TrackReview/ZStackReview object from database + project = Project.get_project_by_id(project_id) + + if not project: + return jsonify({'error': 'project_id not found'}), 404 + + state = load_project_state(project) + # Perform edit operation on the data file + state.action(action_type, info) + + x_changed = state._x_changed + y_changed = state._y_changed + info_changed = state.info_changed + + state._x_changed = state._y_changed = state.info_changed = False + + # Update object in local database + Project.update_project(project, state) + + except Exception as e: # TODO: more error handling to identify problem + traceback.print_exc() + return jsonify({'error': str(e)}), 500 + + tracks = state.readable_tracks if info_changed else False + + if x_changed or y_changed: + encode = lambda x: base64.encodebytes(x.read()).decode() + img_payload = {} + + if x_changed: + raw = state.get_frame(frame, raw=True) + img_payload['raw'] = f'data:image/png;base64,{encode(raw)}' + if y_changed: + img = state.get_frame(frame, raw=False) + img_payload['segmented'] = f'data:image/png;base64,{encode(img)}' + edit_arr = state.get_array(frame) + img_payload['seg_arr'] = edit_arr.tolist() + + else: + img_payload = False + + current_app.logger.debug('Action "%s" for project "%s" finished in %s s.', + action_type, project_id, + timeit.default_timer() - start) + + return jsonify({'tracks': tracks, 'imgs': img_payload}) + + +@bp.route('/frame//') +def get_frame(frame, project_id): + ''' Serve modes of frames as pngs. Send pngs and color mappings of + cells to .js file. + ''' + start = timeit.default_timer() + # Use id to grab appropriate TrackReview/ZStackReview object from database + project = Project.get_project_by_id(project_id) + + if not project: + return jsonify({'error': 'project_id not found'}), 404 + + state = load_project_state(project) + + # Obtain raw, mask, and edit mode frames + img = state.get_frame(frame, raw=False) + raw = state.get_frame(frame, raw=True) + + # Obtain color map of the cells + edit_arr = state.get_array(frame) + + encode = lambda x: base64.encodebytes(x.read()).decode() + + payload = { + 'raw': f'data:image/png;base64,{encode(raw)}', + 'segmented': f'data:image/png;base64,{encode(img)}', + 'seg_arr': edit_arr.tolist() + } + + current_app.logger.debug('Got frame %s of project "%s" in %s s.', + frame, project_id, timeit.default_timer() - start) + + return jsonify(payload) + + +@bp.route('/load/', methods=['POST']) +def load(filename): + ''' Initate TrackReview/ZStackReview object and load object to database. + Send specific attributes of the object to the .js file. + ''' + start = timeit.default_timer() + current_app.logger.info('Loading track at %s', filename) + + folders = re.split('__', filename) + filename = folders[len(folders) - 1] + subfolders = folders[2:len(folders) - 1] + + subfolders = '/'.join(subfolders) + full_path = os.path.join(subfolders, filename) + + input_bucket = folders[0] + output_bucket = folders[1] + + if is_trk_file(filename): + # Initate TrackReview object and entry in database + track_review = TrackReview(filename, input_bucket, output_bucket, full_path) + project = Project.create_project(filename, track_review, subfolders) + current_app.logger.debug('Loaded trk file "%s" in %s s.', + filename, timeit.default_timer() - start) + # Send attributes to .js file + return jsonify({ + 'max_frames': track_review.max_frames, + 'tracks': track_review.readable_tracks, + 'dimensions': track_review.dimensions, + 'project_id': project.id, + 'screen_scale': track_review.scale_factor + }) + + if is_npz_file(filename): + # arg is 'false' which gets parsed to True if casting to bool + rgb = request.args.get('rgb', default='false', type=str) + rgb = bool(distutils.util.strtobool(rgb)) + # Initate ZStackReview object and entry in database + zstack_review = ZStackReview(filename, input_bucket, output_bucket, full_path, rgb) + project = Project.create_project(filename, zstack_review, subfolders) + current_app.logger.debug('Loaded npz file "%s" in %s s.', + filename, timeit.default_timer() - start) + # Send attributes to .js file + return jsonify({ + 'max_frames': zstack_review.max_frames, + 'channel_max': zstack_review.channel_max, + 'feature_max': zstack_review.feature_max, + 'tracks': zstack_review.readable_tracks, + 'dimensions': zstack_review.dimensions, + 'project_id': project.id + }) + + error = { + 'error': 'invalid file extension: {}'.format( + os.path.splitext(filename)[-1]) + } + return jsonify(error), 400 + + +@bp.route('/', methods=['GET', 'POST']) +def form(): + '''Request HTML landing page to be rendered.''' + return render_template('index.html') + + +@bp.route('/tool', methods=['GET', 'POST']) +def tool(): + ''' Request HTML caliban tool page to be rendered after user inputs + filename in the landing page. + ''' + if 'filename' not in request.form: + return redirect('/') + + filename = request.form['filename'] + + current_app.logger.info('%s is filename', filename) + + # TODO: better name template? + new_filename = 'caliban-input__caliban-output__test__{}'.format(filename) + + # if no options passed (how this route will be for now), + # still want to pass in default settings + rgb = request.args.get('rgb', default='false', type=str) + pixel_only = request.args.get('pixel_only', default='false', type=str) + label_only = request.args.get('label_only', default='false', type=str) + + # Using distutils to cast string arguments to bools + settings = { + 'rgb': bool(distutils.util.strtobool(rgb)), + 'pixel_only': bool(distutils.util.strtobool(pixel_only)), + 'label_only': bool(distutils.util.strtobool(label_only)) + } + + if is_trk_file(new_filename): + filetype = 'track' + title = 'Tracking Tool' + + elif is_npz_file(new_filename): + filetype = 'zstack' + title = 'Z-Stack Tool' + + else: + # TODO: render an error template instead of JSON. + error = { + 'error': 'invalid file extension: {}'.format( + os.path.splitext(filename)[-1]) + } + return jsonify(error), 400 + + return render_template( + 'tool.html', + filetype=filetype, + title=title, + filename=new_filename, + settings=settings) + + +@bp.route('/', methods=['GET', 'POST']) +def shortcut(filename): + ''' Request HTML caliban tool page to be rendered if user makes a URL + request to access a specific data file that has been preloaded to the + input S3 bucket (ex. http://127.0.0.1:5000/test.npz). + ''' + rgb = request.args.get('rgb', default='false', type=str) + pixel_only = request.args.get('pixel_only', default='false', type=str) + label_only = request.args.get('label_only', default='false', type=str) + + settings = { + 'rgb': bool(distutils.util.strtobool(rgb)), + 'pixel_only': bool(distutils.util.strtobool(pixel_only)), + 'label_only': bool(distutils.util.strtobool(label_only)) + } + + if is_trk_file(filename): + filetype = 'track' + title = 'Tracking Tool' + + elif is_npz_file(filename): + filetype = 'zstack' + title = 'Z-Stack Tool' + + else: + # TODO: render an error template instead of JSON. + error = { + 'error': 'invalid file extension: {}'.format( + os.path.splitext(filename)[-1]) + } + return jsonify(error), 400 + + return render_template( + 'tool.html', + filetype=filetype, + title=title, + filename=filename, + settings=settings) diff --git a/browser/blueprints_test.py b/browser/blueprints_test.py new file mode 100644 index 000000000..dbf3089e9 --- /dev/null +++ b/browser/blueprints_test.py @@ -0,0 +1,176 @@ +"""Test for Caliban Blueprints""" + +import io + +import pytest + +# from flask_sqlalchemy import SQLAlchemy + +import models + + +class Bunch(object): + def __init__(self, **kwds): + self.__dict__.update(kwds) + + +class DummyState(io.BytesIO): + + def __init__(self, *_, **__): + super().__init__() + + def __getattr__(self, *_, **__): + return self + + def __call__(self, *_, **__): + return self + + def get_frame(self, frame, raw=False): + return io.BytesIO() + + +def test_health(client, mocker): + response = client.get('/health') + assert response.status_code == 200 + assert response.json.get('message') == 'success' + + +def test_create(client): + pass + + +def test_upload_file(client): + response = client.get('/upload_file/1') + assert response.status_code == 404 + + filename_npz = 'filename.npz' + filename_trk = 'filename.trk' + state = DummyState() + subfolders = 'subfolders' + + # Create a project. + project = models.Project.create_project( + filename=filename_npz, + state=state, + subfolders=subfolders) + + response = client.get('/upload_file/{}'.format(project.id)) + assert response.status_code == 302 + + project = models.Project.create_project( + filename=filename_trk, + state=state, + subfolders=subfolders) + + response = client.get('/upload_file/{}'.format(project.id)) + assert response.status_code == 302 + + +def test_get_frame(client): + response = client.get('/frame/0/999999') + assert response.status_code == 404 + + for filename in ('filename.npz', 'filename.trk'): + + # Create a project. + project = models.Project.create_project( + filename=filename, + state=DummyState(), + subfolders='subfolders') + + response = client.get('/frame/0/{}'.format(project.id)) + + # TODO: test correctness + assert 'raw' in response.json + assert 'segmented' in response.json + assert 'seg_arr' in response.json + + # test handle error + project = models.Project.create_project( + filename=filename, + state='invalid state data', + subfolders='subfolders') + + response = client.get('/frame/0/{}'.format(project.id)) + assert response.status_code == 500 + + +def test_action(client): + pass + + +def test_load(client, mocker): + # TODO: parsing the filename is a bit awkward. + in_bucket = 'inputBucket' + out_bucket = 'inputBucket' + base_filename = 'testfile' + basefile = '{}__{}__{}__{}__{}'.format( + in_bucket, out_bucket, 'subfolder1', 'subfolder2', base_filename + ) + + mocker.patch('blueprints.TrackReview', DummyState) + mocker.patch('blueprints.ZStackReview', DummyState) + + # TODO: correctness tests + response = client.post('/load/{}.npz'.format(basefile)) + assert response.status_code == 200 + + # rgb mode only for npzs. + response = client.post('/load/{}.npz?rgb=true'.format(basefile)) + assert response.status_code == 200 + + response = client.post('/load/{}.trk'.format(basefile)) + assert response.status_code == 200 + + response = client.post('/load/{}.badext'.format(basefile)) + assert response.status_code == 400 + + +def test_tool(client): + # test no form redirect + response = client.get('/tool') + assert response.status_code == 302 + + filename = 'test-file.npz' + response = client.post('/tool', + content_type='multipart/form-data', + data={'filename': filename}) + assert response.status_code == 200 + assert b'' in response.data + + filename = 'test-file.trk' + response = client.post('/tool', + content_type='multipart/form-data', + data={'filename': filename}) + assert response.status_code == 200 + assert b'' in response.data + + filename = 'test-file.badext' + response = client.post('/tool', + content_type='multipart/form-data', + data={'filename': filename}) + assert response.status_code == 400 + assert 'error' in response.json + + +def test_shortcut(client): + options = 'rgb=true&pixel_only=true&label_only=true' + response = client.get('/test-file.npz') + assert response.status_code == 200 + assert b'' in response.data + + response = client.get('/test-file.npz?{}'.format(options)) + assert response.status_code == 200 + assert b'' in response.data + + response = client.get('/test-file.trk') + assert response.status_code == 200 + assert b'' in response.data + + response = client.get('/test-file.trk?{}'.format(options)) + assert response.status_code == 200 + assert b'' in response.data + + response = client.get('/test-file.badext') + assert response.status_code == 400 + assert 'error' in response.json diff --git a/browser/caliban.py b/browser/caliban.py index 82cf7b4a4..4859c5f9e 100644 --- a/browser/caliban.py +++ b/browser/caliban.py @@ -1,63 +1,70 @@ -from io import BytesIO - -from imgutils import pngify -from matplotlib.colors import hsv_to_rgb, LinearSegmentedColormap -import matplotlib.pyplot as plt -from random import random +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function import io import copy import json -import matplotlib -import numpy as np import os -import random +import sys import tarfile import tempfile + import boto3 -import sys -from werkzeug.utils import secure_filename +import matplotlib.pyplot as plt +import numpy as np from skimage import filters -import skimage.morphology -from skimage.morphology import watershed, dilation, disk from skimage.morphology import flood_fill, flood +from skimage.morphology import watershed, dilation, disk from skimage.draw import circle from skimage.measure import regionprops from skimage.exposure import rescale_intensity +from skimage.segmentation import find_boundaries -from config import S3_KEY, S3_SECRET +from imgutils import pngify +from config import AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY -# Connect to the s3 service -s3 = boto3.client( - "s3", - aws_access_key_id=S3_KEY, - aws_secret_access_key=S3_SECRET -) class ZStackReview: - def __init__(self, filename, input_bucket, output_bucket, subfolders): + def __init__(self, filename, input_bucket, output_bucket, subfolders, rgb=False): self.filename = filename self.input_bucket = input_bucket self.output_bucket = output_bucket + # subfolders is actually the full file path self.subfolders = subfolders + self.rgb = rgb self.trial = self.load(filename) self.raw = self.trial["raw"] self.annotated = self.trial["annotated"] self.feature = 0 self.feature_max = self.annotated.shape[-1] self.channel = 0 - self.max_frames, self.height, self.width, self.channel_max = self.raw.shape + + self.current_frame = 0 + + if self.rgb: + # possible differences between single channel and rgb displays + self.dims = len(self.raw.shape) + if self.dims == 3: + self.raw = np.expand_dims(self.raw, axis=0) + self.annotated = np.expand_dims(self.annotated, axis=0) + self.max_frames, self.height, self.width, self.rgb_channels = self.raw.shape + self.channel_max = 1 + + self.rescale_raw() + self.reduce_to_RGB() + + else: + self.max_frames, self.height, self.width, self.channel_max = self.raw.shape + self.dimensions = (self.width, self.height) - #create a dictionary that has frame information about each cell - #analogous to .trk lineage but do not need relationships between cells included + # create a dictionary that has frame information about each cell + # analogous to .trk lineage but do not need relationships between cells included self.cell_ids = {} - self.num_cells = {} self.cell_info = {} - self.current_frame = 0 - for feature in range(self.feature_max): self.create_cell_info(feature) @@ -67,16 +74,97 @@ def __init__(self, filename, input_bucket, output_bucket, subfolders): self.max_intensity[channel] = None self.dtype_raw = self.raw.dtype - self.scale_factor = 2 self.save_version = 0 self.color_map = plt.get_cmap('viridis') self.color_map.set_bad('black') - self.frames_changed = False + self._x_changed = False + self._y_changed = False self.info_changed = False + def _get_s3_client(self): + return boto3.client( + 's3', + aws_access_key_id=AWS_ACCESS_KEY_ID, + aws_secret_access_key=AWS_SECRET_ACCESS_KEY + ) + + def get_max_label(self): + ''' + Helper function that returns the highest label in use in currently-viewed + feature. If feature is empty, returns 0 to prevent other functions from crashing. + ''' + # check this first, np.max of empty array will crash + if len(self.cell_ids[self.feature]) == 0: + max_label = 0 + # if any labels exist in feature, find the max label + else: + max_label = int(np.max(self.cell_ids[self.feature])) + return max_label + + def rescale_95(self, img): + ''' + Helper function for rescaling an image. Image can be single- + or multi-channel. + ''' + percentiles = np.percentile(img[img > 0], [5, 95]) + rescaled_img = rescale_intensity( + img, + in_range=(percentiles[0], percentiles[1]), + out_range='uint8') + rescaled_img = rescaled_img.astype('uint8') + return rescaled_img + + def rescale_raw(self): + ''' + Rescale first 6 raw channels individually and store in memory. + The rescaled raw array is used subsequently for image display purposes. + ''' + self.rescaled = np.zeros((self.height, self.width, self.rgb_channels), dtype='uint8') + # this approach allows noise through + for channel in range(min(6, self.rgb_channels)): + raw_channel = self.raw[self.current_frame, :, :, channel] + if np.sum(raw_channel) == 0: + # don't rescale empty channels + pass + else: + self.rescaled[:, :, channel] = self.rescale_95(raw_channel) + + def reduce_to_RGB(self): + ''' + Go from rescaled raw array with up to 6 channels to an RGB image for display. + Handles adding in CMY channels as needed, and adjusting each channel if + viewing adjusted raw. Used to update self.rgb, which is used to display + raw current frame. + ''' + # rgb starts as uint16 so it can handle values above 255 without overflow + self.rgb_img = np.zeros((self.height, self.width, 3), dtype='uint16') + + # for each of the channels that we have + for c in range(min(6, self.rgb_channels)): + # straightforward RGB -> RGB + if c < 3: + self.rgb_img[:, :, c] = (self.rescaled[:, :, c]).astype('uint16') + # collapse cyan to G and B + if c == 3: + self.rgb_img[:, :, 1] += (self.rescaled[:, :, 3]).astype('uint16') + self.rgb_img[:, :, 2] += (self.rescaled[:, :, 3]).astype('uint16') + # collapse magenta to R and B + if c == 4: + self.rgb_img[:, :, 0] += (self.rescaled[:, :, 4]).astype('uint16') + self.rgb_img[:, :, 2] += (self.rescaled[:, :, 4]).astype('uint16') + # collapse yellow to R and G + if c == 5: + self.rgb_img[:, :, 0] += (self.rescaled[:, :, 5]).astype('uint16') + self.rgb_img[:, :, 1] += (self.rescaled[:, :, 5]).astype('uint16') + + # clip values to uint8 range so it can be cast without overflow + self.rgb_img[:, :, 0:3] = np.clip(self.rgb_img[:, :, 0:3], a_min=0, a_max=255) + + self.rgb_img = self.rgb_img.astype('uint8') + @property def readable_tracks(self): """ @@ -89,39 +177,59 @@ def readable_tracks(self): for _, label in feature.items(): slices = list(map(list, consecutive(label['frames']))) slices = '[' + ', '.join(["{}".format(a[0]) - if len(a) == 1 else "{}-{}".format(a[0], a[-1]) - for a in slices]) + ']' - label["slices"] = str(slices) + if len(a) == 1 else "{}-{}".format(a[0], a[-1]) + for a in slices]) + ']' + label['slices'] = str(slices) return cell_info def get_frame(self, frame, raw): - if raw: - frame = self.raw[frame][:,:, self.channel] + if (raw and not self.rgb): + frame = self.raw[frame][:, :, self.channel] return pngify(imgarr=frame, vmin=0, vmax=self.max_intensity[self.channel], cmap="cubehelix") + elif (raw and self.rgb): + frame = self.rgb_img + return pngify(imgarr=frame, + vmin=None, + vmax=None, + cmap=None) + else: - frame = self.annotated[frame][:,:, self.feature] + frame = self.annotated[frame][:, :, self.feature] frame = np.ma.masked_equal(frame, 0) return pngify(imgarr=frame, - vmin=0, - vmax=np.max(self.cell_ids[self.feature]), - cmap=self.color_map) + vmin=0, + vmax=self.get_max_label(), + cmap=self.color_map) def get_array(self, frame): - frame = self.annotated[frame][:,:,self.feature] + frame = self.annotated[frame][:, :, self.feature] + frame = self.add_outlines(frame) return frame - def load(self, filename): + def add_outlines(self, frame): + ''' + Helper function that indicates label outlines in array with + negative values of that label. + ''' + # this is sometimes int 32 but may be uint, convert to + # int16 to ensure negative numbers and smaller payload than int32 + frame = frame.astype(np.int16) + boundary_mask = find_boundaries(frame, mode='inner') + outlined_frame = np.where(boundary_mask == 1, -frame, frame) + return outlined_frame + def load(self, filename): + # TODO: we should try to not use global if possible. global original_filename original_filename = filename - s3 = boto3.client('s3') + s3 = self._get_s3_client() key = self.subfolders print(key) - response = s3.get_object(Bucket=self.input_bucket, Key= key) + response = s3.get_object(Bucket=self.input_bucket, Key=key) return load_npz(response['Body'].read()) def action(self, action_type, info): @@ -177,29 +285,29 @@ def action(self, action_type, info): def action_change_channel(self, channel): self.channel = channel - self.frames_changed = True + self._x_changed = True def action_change_feature(self, feature): self.feature = feature - self.frames_changed = True + self._y_changed = True def action_handle_draw(self, trace, target_value, brush_value, brush_size, erase, frame): - annotated = np.copy(self.annotated[frame,:,:,self.feature]) + annotated = np.copy(self.annotated[frame, :, :, self.feature]) in_original = np.any(np.isin(annotated, brush_value)) - annotated_draw = np.where(annotated==target_value, brush_value, annotated) - annotated_erase = np.where(annotated==brush_value, target_value, annotated) + annotated_draw = np.where(annotated == target_value, brush_value, annotated) + annotated_erase = np.where(annotated == brush_value, target_value, annotated) for loc in trace: # each element of trace is an array with [y,x] coordinates of array x_loc = loc[1] y_loc = loc[0] - brush_area = circle(y_loc, x_loc, brush_size, (self.height,self.width)) + brush_area = circle(y_loc, x_loc, brush_size, (self.height, self.width)) - #do not overwrite or erase labels other than the one you're editing + # do not overwrite or erase labels other than the one you're editing if not erase: annotated[brush_area] = annotated_draw[brush_area] else: @@ -207,20 +315,20 @@ def action_handle_draw(self, trace, target_value, brush_value, brush_size, erase in_modified = np.any(np.isin(annotated, brush_value)) - #cell deletion + # cell deletion if in_original and not in_modified: - self.del_cell_info(feature = self.feature, del_label = brush_value, frame = frame) + self.del_cell_info(feature=self.feature, del_label=brush_value, frame=frame) - #cell addition + # cell addition elif in_modified and not in_original: - self.add_cell_info(feature = self.feature, add_label = brush_value, frame = frame) + self.add_cell_info(feature=self.feature, add_label=brush_value, frame=frame) - #check for image change, in case pixels changed but no new or del cell - comparison = np.where(annotated != self.annotated[frame,:,:,self.feature]) - self.frames_changed = np.any(comparison) - #if info changed, self.info_changed set to true with info helper functions + # check for image change, in case pixels changed but no new or del cell + comparison = np.where(annotated != self.annotated[frame, :, :, self.feature]) + self._y_changed = np.any(comparison) + # if info changed, self.info_changed set to true with info helper functions - self.annotated[frame,:,:,self.feature] = annotated + self.annotated[frame, :, :, self.feature] = annotated def action_threshold(self, y1, x1, y2, x2, frame, label): ''' @@ -237,44 +345,48 @@ def action_threshold(self, y1, x1, y2, x2, frame, label): # triangle threshold picked after trying a few on one dataset # may not be the best threshold approach for other datasets! # pick two thresholds to use hysteresis thresholding strategy - threshold = filters.threshold_triangle(image = predict_area) + threshold = filters.threshold_triangle(image=predict_area) threshold_stringent = 1.10 * threshold # try to keep stray pixels from appearing - hyst = filters.apply_hysteresis_threshold(image = predict_area, low = threshold, high = threshold_stringent) + hyst = filters.apply_hysteresis_threshold(image=predict_area, + low=threshold, + high=threshold_stringent) ann_threshold = np.where(hyst, label, 0) - #put prediction in without overwriting - predict_area = self.annotated[frame, top_edge:bottom_edge, left_edge:right_edge, self.feature] + # put prediction in without overwriting + predict_area = self.annotated[frame, top_edge:bottom_edge, + left_edge:right_edge, self.feature] safe_overlay = np.where(predict_area == 0, ann_threshold, predict_area) - self.annotated[frame,top_edge:bottom_edge,left_edge:right_edge,self.feature] = safe_overlay + self.annotated[frame, top_edge:bottom_edge, + left_edge:right_edge, self.feature] = safe_overlay # don't need to update cell_info unless an annotation has been added - if np.any(np.isin(self.annotated[frame,:,:,self.feature], label)): - self.add_cell_info(feature=self.feature, add_label=label, frame = frame) + if np.any(np.isin(self.annotated[frame, :, :, self.feature], label)): + self.add_cell_info(feature=self.feature, add_label=label, frame=frame) def action_flood_contiguous(self, label, frame, x_location, y_location): ''' flood fill a cell with a unique new label; alternative to watershed for fixing duplicate label issue if cells are not touching ''' - img_ann = self.annotated[frame,:,:,self.feature] + img_ann = self.annotated[frame, :, :, self.feature] old_label = label new_label = np.max(self.cell_ids[self.feature]) + 1 in_original = np.any(np.isin(img_ann, old_label)) - filled_img_ann = flood_fill(img_ann, (int(y_location/self.scale_factor), int(x_location/self.scale_factor)), new_label) - self.annotated[frame,:,:,self.feature] = filled_img_ann + filled_img_ann = flood_fill(img_ann, (y_location, x_location), new_label) + self.annotated[frame, :, :, self.feature] = filled_img_ann in_modified = np.any(np.isin(filled_img_ann, old_label)) # update cell info dicts since labels are changing - self.add_cell_info(feature=self.feature, add_label=new_label, frame = frame) + self.add_cell_info(feature=self.feature, add_label=new_label, frame=frame) if in_original and not in_modified: - self.del_cell_info(feature = self.feature, del_label = old_label, frame = frame) + self.del_cell_info(feature=self.feature, del_label=old_label, frame=frame) def action_trim_pixels(self, label, frame, x_location, y_location): ''' @@ -282,16 +394,16 @@ def action_trim_pixels(self, label, frame, x_location, y_location): that are not connected to the cell selected will be removed from annotation in that frame ''' - img_ann = self.annotated[frame,:,:,self.feature] - contig_cell = flood(image = img_ann, seed_point = (int(y_location/self.scale_factor), int(x_location/self.scale_factor))) + img_ann = self.annotated[frame, :, :, self.feature] + contig_cell = flood(image=img_ann, seed_point=(y_location, x_location)) img_trimmed = np.where(np.logical_and(np.invert(contig_cell), img_ann == label), 0, img_ann) - #check if image changed + # check if image changed comparison = np.where(img_trimmed != img_ann) - self.frames_changed = np.any(comparison) - #this action should never change the cell info + self._y_changed = np.any(comparison) + # this action should never change the cell info - self.annotated[frame,:,:,self.feature] = img_trimmed + self.annotated[frame, :, :, self.feature] = img_trimmed def action_fill_hole(self, label, frame, x_location, y_location): ''' @@ -301,15 +413,14 @@ def action_fill_hole(self, label, frame, x_location, y_location): size, then fills the hole with label (using skimage flood_fill). connectivity = 1 prevents hole fill from spilling out into background in some cases ''' - # rescale click location -> corresponding location in annotation array - hole_fill_seed = (y_location // self.scale_factor, x_location // self.scale_factor) + hole_fill_seed = (y_location, x_location) # fill hole with label - img_ann = self.annotated[frame,:,:,self.feature] - filled_img_ann = flood_fill(img_ann, hole_fill_seed, label, connectivity = 1) - self.annotated[frame,:,:,self.feature] = filled_img_ann + img_ann = self.annotated[frame, :, :, self.feature] + filled_img_ann = flood_fill(img_ann, hole_fill_seed, label, connectivity=1) + self.annotated[frame, :, :, self.feature] = filled_img_ann - #never changes info but always changes annotation - self.frames_changed = True + # never changes info but always changes annotation + self._y_changed = True def action_new_single_cell(self, label, frame): """ @@ -319,12 +430,12 @@ def action_new_single_cell(self, label, frame): new_label = np.max(self.cell_ids[self.feature]) + 1 # replace frame labels - frame = self.annotated[single_frame,:,:,self.feature] + frame = self.annotated[single_frame, :, :, self.feature] frame[frame == old_label] = new_label # replace fields - self.del_cell_info(feature = self.feature, del_label = old_label, frame = single_frame) - self.add_cell_info(feature = self.feature, add_label = new_label, frame = single_frame) + self.del_cell_info(feature=self.feature, del_label=old_label, frame=single_frame) + self.add_cell_info(feature=self.feature, add_label=new_label, frame=single_frame) def action_new_cell_stack(self, label, frame): @@ -335,26 +446,26 @@ def action_new_cell_stack(self, label, frame): new_label = np.max(self.cell_ids[self.feature]) + 1 # replace frame labels - for frame in self.annotated[start_frame:,:,:,self.feature]: + for frame in self.annotated[start_frame:, :, :, self.feature]: frame[frame == old_label] = new_label for frame in range(self.annotated.shape[0]): - if new_label in self.annotated[frame,:,:,self.feature]: - self.del_cell_info(feature = self.feature, del_label = old_label, frame = frame) - self.add_cell_info(feature = self.feature, add_label = new_label, frame = frame) + if new_label in self.annotated[frame, :, :, self.feature]: + self.del_cell_info(feature=self.feature, del_label=old_label, frame=frame) + self.add_cell_info(feature=self.feature, add_label=new_label, frame=frame) def action_delete_mask(self, label, frame): ''' remove selected annotation from frame, replacing with zeros ''' - ann_img = self.annotated[frame,:,:,self.feature] + ann_img = self.annotated[frame, :, :, self.feature] ann_img = np.where(ann_img == label, 0, ann_img) - self.annotated[frame,:,:,self.feature] = ann_img + self.annotated[frame, :, :, self.feature] = ann_img - #update cell_info - self.del_cell_info(feature = self.feature, del_label = label, frame = frame) + # update cell_info + self.del_cell_info(feature=self.feature, del_label=label, frame=frame) def action_replace_single(self, label_1, label_2, frame): ''' @@ -362,13 +473,13 @@ def action_replace_single(self, label_1, label_2, frame): to make sure labels are different and were selected within same frames before sending action ''' - annotated = self.annotated[frame,:,:,self.feature] + annotated = self.annotated[frame, :, :, self.feature] # change annotation annotated = np.where(annotated == label_2, label_1, annotated) - self.annotated[frame,:,:,self.feature] = annotated + self.annotated[frame, :, :, self.feature] = annotated # update info - self.add_cell_info(feature = self.feature, add_label = label_1, frame = frame) - self.del_cell_info(feature = self.feature, del_label = label_2, frame = frame) + self.add_cell_info(feature=self.feature, add_label=label_1, frame=frame) + self.del_cell_info(feature=self.feature, del_label=label_2, frame=frame) def action_replace(self, label_1, label_2): """ @@ -377,41 +488,41 @@ def action_replace(self, label_1, label_2): """ # check each frame for frame in range(self.annotated.shape[0]): - annotated = self.annotated[frame,:,:,self.feature] + annotated = self.annotated[frame, :, :, self.feature] # if label being replaced is present, remove it from image and update cell info dict if np.any(np.isin(annotated, label_2)): annotated = np.where(annotated == label_2, label_1, annotated) - self.annotated[frame,:,:,self.feature] = annotated - self.add_cell_info(feature = self.feature, add_label = label_1, frame = frame) - self.del_cell_info(feature = self.feature, del_label = label_2, frame = frame) + self.annotated[frame, :, :, self.feature] = annotated + self.add_cell_info(feature=self.feature, add_label=label_1, frame=frame) + self.del_cell_info(feature=self.feature, del_label=label_2, frame=frame) def action_swap_single_frame(self, label_1, label_2, frame): - ann_img = self.annotated[frame,:,:,self.feature] + ann_img = self.annotated[frame, :, :, self.feature] ann_img = np.where(ann_img == label_1, -1, ann_img) ann_img = np.where(ann_img == label_2, label_1, ann_img) ann_img = np.where(ann_img == -1, label_2, ann_img) - self.annotated[frame,:,:,self.feature] = ann_img + self.annotated[frame, :, :, self.feature] = ann_img - self.frames_changed = self.info_changed = True + self._y_changed = self.info_changed = True def action_swap_all_frame(self, label_1, label_2): for frame in range(self.annotated.shape[0]): - ann_img = self.annotated[frame,:,:,self.feature] + ann_img = self.annotated[frame, :, :, self.feature] ann_img = np.where(ann_img == label_1, -1, ann_img) ann_img = np.where(ann_img == label_2, label_1, ann_img) ann_img = np.where(ann_img == -1, label_2, ann_img) - self.annotated[frame,:,:,self.feature] = ann_img + self.annotated[frame, :, :, self.feature] = ann_img - #update cell_info + # update cell_info cell_info_1 = self.cell_info[self.feature][label_1].copy() cell_info_2 = self.cell_info[self.feature][label_2].copy() self.cell_info[self.feature][label_1].update({'frames': cell_info_2['frames']}) self.cell_info[self.feature][label_2].update({'frames': cell_info_1['frames']}) - self.frames_changed = self.info_changed = True + self._y_changed = self.info_changed = True def action_watershed(self, label, frame, x1_location, y1_location, x2_location, y2_location): # Pull the label that is being split and find a new valid label @@ -419,17 +530,18 @@ def action_watershed(self, label, frame, x1_location, y1_location, x2_location, new_label = np.max(self.cell_ids[self.feature]) + 1 # Locally store the frames to work on - img_raw = self.raw[frame,:,:,self.channel] - img_ann = self.annotated[frame,:,:,self.feature] + img_raw = self.raw[frame, :, :, self.channel] + img_ann = self.annotated[frame, :, :, self.feature] # Pull the 2 seed locations and store locally # define a new seeds labeled img that is the same size as raw/annotation imgs seeds_labeled = np.zeros(img_ann.shape) # create two seed locations - seeds_labeled[int(y1_location/self.scale_factor ), int(x1_location/self.scale_factor)]=current_label - seeds_labeled[int(y2_location/self.scale_factor ), int(x2_location/self.scale_factor )]=new_label + seeds_labeled[y1_location, x1_location] = current_label + seeds_labeled[y2_location, x2_location] = new_label - # define the bounding box to apply the transform on and select appropriate sections of 3 inputs (raw, seeds, annotation mask) + # define the bounding box to apply the transform on and select + # appropriate sections of 3 inputs (raw, seeds, annotation mask) props = regionprops(np.squeeze(np.int32(img_ann == current_label))) minr, minc, maxr, maxc = props[0].bbox @@ -455,43 +567,43 @@ def action_watershed(self, label, frame, x1_location, y1_location, x2_location, old_pixels = np.count_nonzero(ws == current_label) if old_pixels < 5: # create dilation image so "dimmer" label is not eroded by "brighter" label - dilated_ws = dilation(np.where(ws==current_label, ws, 0), disk(3)) - ws = np.where(dilated_ws==current_label, dilated_ws, ws) + dilated_ws = dilation(np.where(ws == current_label, ws, 0), disk(3)) + ws = np.where(dilated_ws == current_label, dilated_ws, ws) # only update img_sub_ann where ws has changed label from current_label to new_label - img_sub_ann = np.where(np.logical_and(ws == new_label,img_sub_ann == current_label), ws, img_sub_ann) + img_sub_ann = np.where(np.logical_and(ws == new_label, img_sub_ann == current_label), + ws, img_sub_ann) # reintegrate subsection into original mask img_ann[minr:maxr, minc:maxc] = img_sub_ann - self.annotated[frame,:,:,self.feature] = img_ann + self.annotated[frame, :, :, self.feature] = img_ann - #update cell_info dict only if new label was created with ws - if np.any(np.isin(self.annotated[frame,:,:,self.feature], new_label)): - self.add_cell_info(feature=self.feature, add_label=new_label, frame = frame) + # update cell_info dict only if new label was created with ws + if np.any(np.isin(self.annotated[frame, :, :, self.feature], new_label)): + self.add_cell_info(feature=self.feature, add_label=new_label, frame=frame) def action_predict_single(self, frame): - ''' predicts zstack relationship for current frame based on previous frame useful for finetuning corrections one frame at a time ''' - annotated = self.annotated[:,:,:,self.feature] + annotated = self.annotated[:, :, :, self.feature] current_slice = frame if current_slice > 0: prev_slice = current_slice - 1 - img = self.annotated[prev_slice,:,:,self.feature] - next_img = self.annotated[current_slice,:,:,self.feature] + img = self.annotated[prev_slice, :, :, self.feature] + next_img = self.annotated[current_slice, :, :, self.feature] updated_slice = predict_zstack_cell_ids(img, next_img) - #check if image changed + # check if image changed comparison = np.where(next_img != updated_slice) - self.frames_changed = np.any(comparison) + self._y_changed = np.any(comparison) - #if the image changed, update self.annotated and remake cell info - if self.frames_changed: - self.annotated[current_slice,:,:,int(self.feature)] = updated_slice - self.create_cell_info(feature = int(self.feature)) + # if the image changed, update self.annotated and remake cell info + if self._y_changed: + self.annotated[current_slice, :, :, int(self.feature)] = updated_slice + self.create_cell_info(feature=int(self.feature)) def action_predict_zstack(self): ''' @@ -499,34 +611,36 @@ def action_predict_zstack(self): different slices of the same cell ''' - annotated = self.annotated[:,:,:,self.feature] + annotated = self.annotated[:, :, :, self.feature] - for zslice in range(self.annotated.shape[0] -1): - img = self.annotated[zslice,:,:,self.feature] + for zslice in range(self.annotated.shape[0] - 1): + img = self.annotated[zslice, :, :, self.feature] - next_img = self.annotated[zslice + 1,:,:,self.feature] + next_img = self.annotated[zslice + 1, :, :, self.feature] predicted_next = predict_zstack_cell_ids(img, next_img) - self.annotated[zslice + 1,:,:,self.feature] = predicted_next + self.annotated[zslice + 1, :, :, self.feature] = predicted_next - #remake cell_info dict based on new annotations - self.frames_changed = True - self.create_cell_info(feature = self.feature) + # remake cell_info dict based on new annotations + self._y_changed = True + self.create_cell_info(feature=self.feature) def action_save_zstack(self): - save_file = self.filename + "_save_version_{}.npz".format(self.save_version) + # save file to BytesIO object + store_npz = io.BytesIO() - # save secure version of data before storing on regular file system - file = secure_filename(save_file) + # X and y are array names by convention + np.savez(store_npz, X=self.raw, y=self.annotated) + store_npz.seek(0) - np.savez(file, raw = self.raw, annotated = self.annotated) - path = self.subfolders - s3.upload_file(file, self.output_bucket, path) + # store npz file object in bucket/subfolders (subfolders is full path) + s3 = self._get_s3_client() + s3.upload_fileobj(store_npz, self.output_bucket, self.subfolders) def add_cell_info(self, feature, add_label, frame): ''' helper function for actions that add a cell to the npz ''' - #if cell already exists elsewhere in npz: + # if cell already exists elsewhere in npz: add_label = int(add_label) try: @@ -534,7 +648,7 @@ def add_cell_info(self, feature, add_label, frame): updated_frames = np.append(old_frames, frame) updated_frames = np.unique(updated_frames).tolist() self.cell_info[feature][add_label].update({'frames': updated_frames}) - #cell does not exist anywhere in npz: + # cell does not exist anywhere in npz: except KeyError: self.cell_info[feature].update({add_label: {}}) self.cell_info[feature][add_label].update({'label': str(add_label)}) @@ -543,42 +657,38 @@ def add_cell_info(self, feature, add_label, frame): self.cell_ids[feature] = np.append(self.cell_ids[feature], add_label) - self.num_cells[feature] += 1 - - #if adding cell, frames and info have necessarily changed - self.frames_changed = self.info_changed = True + # if adding cell, frames and info have necessarily changed + self._y_changed = self.info_changed = True def del_cell_info(self, feature, del_label, frame): ''' helper function for actions that remove a cell from the npz ''' - #remove cell from frame + # remove cell from frame old_frames = self.cell_info[feature][del_label]['frames'] updated_frames = np.delete(old_frames, np.where(old_frames == np.int64(frame))).tolist() self.cell_info[feature][del_label].update({'frames': updated_frames}) - #if that was the last frame, delete the entry for that cell + # if that was the last frame, delete the entry for that cell if self.cell_info[feature][del_label]['frames'] == []: del self.cell_info[feature][del_label] - #also remove from list of cell_ids + # also remove from list of cell_ids ids = self.cell_ids[feature] self.cell_ids[feature] = np.delete(ids, np.where(ids == np.int64(del_label))) - #if deleting cell, frames and info have necessarily changed - self.frames_changed = self.info_changed = True + # if deleting cell, frames and info have necessarily changed + self._y_changed = self.info_changed = True def create_cell_info(self, feature): ''' helper function for actions that make or remake the entire cell info dict ''' feature = int(feature) - annotated = self.annotated[:,:,:,feature] + annotated = self.annotated[:, :, :, feature] self.cell_ids[feature] = np.unique(annotated)[np.nonzero(np.unique(annotated))] - self.num_cells[feature] = int(max(self.cell_ids[feature])) - self.cell_info[feature] = {} for cell in self.cell_ids[feature]: @@ -589,7 +699,7 @@ def create_cell_info(self, feature): self.cell_info[feature][cell]['frames'] = [] for frame in range(self.annotated.shape[0]): - if cell in annotated[frame,:,:]: + if cell in annotated[frame, :, :]: self.cell_info[feature][cell]['frames'].append(int(frame)) self.cell_info[feature][cell]['slices'] = '' @@ -597,7 +707,7 @@ def create_cell_info(self, feature): def create_lineage(self): for cell in self.cell_ids[self.feature]: - self.lineage[str(cell)] = {} + self.lineage[str(cell)] = {} # TODO: what is self.lineage? cell_info = self.lineage[str(cell)] cell_info["label"] = int(cell) @@ -608,9 +718,6 @@ def create_lineage(self): cell_info["frames"] = self.cell_info[self.feature][cell]['frames'] - -#_______________________________________________________________________________________________________________ - class TrackReview: def __init__(self, filename, input_bucket, output_bucket, subfolders): self.filename = filename @@ -639,9 +746,17 @@ def __init__(self, filename, input_bucket, output_bucket, subfolders): self.current_frame = 0 - self.frames_changed = False + self._x_changed = False + self._y_changed = False self.info_changed = False + def _get_s3_client(self): + return boto3.client( + 's3', + aws_access_key_id=AWS_ACCESS_KEY_ID, + aws_secret_access_key=AWS_SECRET_ACCESS_KEY + ) + @property def readable_tracks(self): """ @@ -653,8 +768,8 @@ def readable_tracks(self): for _, track in tracks.items(): frames = list(map(list, consecutive(track["frames"]))) frames = '[' + ', '.join(["{}".format(a[0]) - if len(a) == 1 else "{}-{}".format(a[0], a[-1]) - for a in frames]) + ']' + if len(a) == 1 else "{}-{}".format(a[0], a[-1]) + for a in frames]) + ']' track["frames"] = frames return tracks @@ -662,27 +777,27 @@ def readable_tracks(self): def get_frame(self, frame, raw): self.current_frame = frame if raw: - frame = self.raw[frame][:,:,0] + frame = self.raw[frame][:, :, 0] return pngify(imgarr=frame, vmin=0, vmax=None, cmap="cubehelix") else: - frame = self.tracked[frame][:,:,0] + frame = self.tracked[frame][:, :, 0] frame = np.ma.masked_equal(frame, 0) return pngify(imgarr=frame, - vmin=0, - vmax=max(self.tracks), - cmap=self.color_map) + vmin=0, + vmax=max(self.tracks), + cmap=self.color_map) def get_array(self, frame): - frame = self.tracked[frame][:,:,0] + frame = self.tracked[frame][:, :, 0] return frame def load(self, filename): global original_filename original_filename = filename - s3 = boto3.client('s3') + s3 = self._get_s3_client() response = s3.get_object(Bucket=self.input_bucket, Key=self.subfolders) return load_trks(response['Body'].read()) @@ -733,17 +848,17 @@ def action_handle_draw(self, trace, edit_value, brush_size, erase, frame): in_original = np.any(np.isin(annotated, edit_value)) - annotated_draw = np.where(annotated==0, edit_value, annotated) - annotated_erase = np.where(annotated==edit_value, 0, annotated) + annotated_draw = np.where(annotated == 0, edit_value, annotated) + annotated_erase = np.where(annotated == edit_value, 0, annotated) for loc in trace: # each element of trace is an array with [y,x] coordinates of array x_loc = loc[1] y_loc = loc[0] - brush_area = circle(y_loc, x_loc, brush_size, (self.height,self.width)) + brush_area = circle(y_loc, x_loc, brush_size, (self.height, self.width)) - #do not overwrite or erase labels other than the one you're editing + # do not overwrite or erase labels other than the one you're editing if not erase: annotated[brush_area] = annotated_draw[brush_area] else: @@ -753,14 +868,14 @@ def action_handle_draw(self, trace, edit_value, brush_size, erase, frame): # cell deletion if in_original and not in_modified: - self.del_cell_info(del_label = edit_value, frame = frame) + self.del_cell_info(del_label=edit_value, frame=frame) # cell addition elif in_modified and not in_original: - self.add_cell_info(add_label = edit_value, frame = frame) + self.add_cell_info(add_label=edit_value, frame=frame) comparison = np.where(annotated != self.tracked[frame]) - self.frames_changed = np.any(comparison) + self._y_changed = np.any(comparison) self.tracked[frame] = annotated @@ -769,22 +884,25 @@ def action_flood_contiguous(self, label, frame, x_location, y_location): flood fill a cell with a unique new label; alternative to watershed for fixing duplicate label issue if cells are not touching ''' - img_ann = self.tracked[frame,:,:,0] + img_ann = self.tracked[frame, :, :, 0] old_label = label new_label = max(self.tracks) + 1 in_original = np.any(np.isin(img_ann, old_label)) - filled_img_ann = flood_fill(img_ann, (int(y_location/self.scale_factor), int(x_location/self.scale_factor)), new_label) - self.tracked[frame,:,:,0] = filled_img_ann + filled_img_ann = flood_fill(img_ann, + (int(y_location / self.scale_factor), + int(x_location / self.scale_factor)), + new_label) + self.tracked[frame, :, :, 0] = filled_img_ann in_modified = np.any(np.isin(filled_img_ann, old_label)) # update cell info dicts since labels are changing - self.add_cell_info(add_label=new_label, frame = frame) + self.add_cell_info(add_label=new_label, frame=frame) if in_original and not in_modified: - self.del_cell_info(del_label = old_label, frame = frame) + self.del_cell_info(del_label=old_label, frame=frame) def action_trim_pixels(self, label, frame, x_location, y_location): ''' @@ -792,14 +910,15 @@ def action_trim_pixels(self, label, frame, x_location, y_location): that are not connected to the cell selected will be removed from annotation in that frame ''' - img_ann = self.tracked[frame,:,:,0] - contig_cell = flood(image = img_ann, seed_point = (int(y_location/self.scale_factor), int(x_location/self.scale_factor))) + img_ann = self.tracked[frame, :, :, 0] + contig_cell = flood(image=img_ann, seed_point=(int(y_location / self.scale_factor), + int(x_location / self.scale_factor))) img_trimmed = np.where(np.logical_and(np.invert(contig_cell), img_ann == label), 0, img_ann) comparison = np.where(img_trimmed != img_ann) - self.frames_changed = np.any(comparison) + self._y_changed = np.any(comparison) - self.tracked[frame,:,:,0] = img_trimmed + self.tracked[frame, :, :, 0] = img_trimmed def action_fill_hole(self, label, frame, x_location, y_location): ''' @@ -812,11 +931,11 @@ def action_fill_hole(self, label, frame, x_location, y_location): # rescale click location -> corresponding location in annotation array hole_fill_seed = (y_location // self.scale_factor, x_location // self.scale_factor) # fill hole with label - img_ann = self.tracked[frame,:,:,0] - filled_img_ann = flood_fill(img_ann, hole_fill_seed, label, connectivity = 1) - self.tracked[frame,:,:,0] = filled_img_ann + img_ann = self.tracked[frame, :, :, 0] + filled_img_ann = flood_fill(img_ann, hole_fill_seed, label, connectivity=1) + self.tracked[frame, :, :, 0] = filled_img_ann - self.frames_changed = True + self._y_changed = True def action_new_single_cell(self, label, frame): """ @@ -827,14 +946,13 @@ def action_new_single_cell(self, label, frame): # replace frame labels self.tracked[frame] = np.where(self.tracked[frame] == old_label, - new_label, self.tracked[frame]) + new_label, self.tracked[frame]) # replace fields - self.del_cell_info(del_label = old_label, frame = frame) - self.add_cell_info(add_label = new_label, frame = frame) + self.del_cell_info(del_label=old_label, frame=frame) + self.add_cell_info(add_label=new_label, frame=frame) def action_new_track(self, label, frame): - """ Replacing label - create in all subsequent frames """ @@ -843,6 +961,7 @@ def action_new_track(self, label, frame): if start_frame != 0: # replace frame labels + # TODO: which frame is this meant to be? for frame in self.tracked[start_frame:]: frame[frame == old_label] = new_label @@ -873,7 +992,7 @@ def action_new_track(self, label, frame): track_old["frame_div"] = None track_old["capped"] = True - self.frames_changed = self.info_changed = True + self._y_changed = self.info_changed = True def action_delete(self, label, frame): """ @@ -884,7 +1003,7 @@ def action_delete(self, label, frame): ann_img = np.where(ann_img == label, 0, ann_img) self.tracked[frame] = ann_img - self.del_cell_info(del_label = label, frame = frame) + self.del_cell_info(del_label=label, frame=frame) def action_set_parent(self, label_1, label_2): """ @@ -940,20 +1059,20 @@ def action_replace(self, label_1, label_2): except ValueError: pass - self.frames_changed = self.info_changed = True + self._y_changed = self.info_changed = True def action_swap_single_frame(self, label_1, label_2, frame): '''swap the labels of two cells in one frame, but do not change any of the lineage information''' - ann_img = self.tracked[frame,:,:,0] + ann_img = self.tracked[frame, :, :, 0] ann_img = np.where(ann_img == label_1, -1, ann_img) ann_img = np.where(ann_img == label_2, label_1, ann_img) ann_img = np.where(ann_img == -1, label_2, ann_img) - self.tracked[frame,:,:,0] = ann_img + self.tracked[frame, :, :, 0] = ann_img - self.frames_changed = True + self._y_changed = True def action_swap_tracks(self, label_1, label_2): def relabel(old_label, new_label): @@ -977,7 +1096,7 @@ def relabel(old_label, new_label): relabel(label_2, label_1) relabel(-1, label_2) - self.frames_changed = self.info_changed = True + self._y_changed = self.info_changed = True def action_watershed(self, label, frame, x1_location, y1_location, x2_location, y2_location): @@ -986,21 +1105,22 @@ def action_watershed(self, label, frame, x1_location, y1_location, x2_location, new_label = max(self.tracks) + 1 # Locally store the frames to work on - img_raw = self.raw[frame,:,:,0] - img_ann = self.tracked[frame,:,:,0] + img_raw = self.raw[frame, :, :, 0] + img_ann = self.tracked[frame, :, :, 0] # Pull the 2 seed locations and store locally # define a new seeds labeled img that is the same size as raw/annotation imgs seeds_labeled = np.zeros(img_ann.shape) # create two seed locations - seeds_labeled[int(y1_location/self.scale_factor), - int(x1_location/self.scale_factor)] = current_label + seeds_labeled[int(y1_location / self.scale_factor), + int(x1_location / self.scale_factor)] = current_label - seeds_labeled[int(y2_location/self.scale_factor), - int(x2_location/self.scale_factor)] = new_label + seeds_labeled[int(y2_location / self.scale_factor), + int(x2_location / self.scale_factor)] = new_label - # define the bounding box to apply the transform on and select appropriate sections of 3 inputs (raw, seeds, annotation mask) + # define the bounding box to apply the transform on and select + # appropriate sections of 3 inputs (raw, seeds, annotation mask) props = regionprops(np.squeeze(np.int32(img_ann == current_label))) minr, minc, maxr, maxc = props[0].bbox @@ -1026,20 +1146,20 @@ def action_watershed(self, label, frame, x1_location, y1_location, x2_location, old_pixels = np.count_nonzero(ws == current_label) if old_pixels < 5: # create dilation image so "dimmer" label is not eroded by "brighter" label - dilated_ws = dilation(np.where(ws==current_label, ws, 0), disk(3)) - ws = np.where(dilated_ws==current_label, dilated_ws, ws) + dilated_ws = dilation(np.where(ws == current_label, ws, 0), disk(3)) + ws = np.where(dilated_ws == current_label, dilated_ws, ws) # only update img_sub_ann where ws has changed label from current_label to new_label - img_sub_ann = np.where(np.logical_and(ws == new_label,img_sub_ann == current_label), - ws, img_sub_ann) + img_sub_ann = np.where(np.logical_and(ws == new_label, img_sub_ann == current_label), + ws, img_sub_ann) - #reintegrate subsection into original mask + # reintegrate subsection into original mask img_ann[minr:maxr, minc:maxc] = img_sub_ann - self.tracked[frame,:,:,0] = img_ann + self.tracked[frame, :, :, 0] = img_ann - #update cell_info dict only if new label was created with ws - if np.any(np.isin(self.tracked[frame,:,:,0], new_label)): - self.add_cell_info(add_label=new_label, frame = frame) + # update cell_info dict only if new label was created with ws + if np.any(np.isin(self.tracked[frame, :, :, 0], new_label)): + self.add_cell_info(add_label=new_label, frame=frame) def action_save_track(self): # clear any empty tracks before saving file @@ -1050,9 +1170,10 @@ def action_save_track(self): for track in empty_tracks: del self.tracks[track] - file = secure_filename(self.filename) + # create file object in memory instead of writing to disk + trk_file_obj = io.BytesIO() - with tarfile.open(file, "w") as trks: + with tarfile.open(fileobj=trk_file_obj, mode="w") as trks: with tempfile.NamedTemporaryFile("w") as lineage_file: json.dump(self.tracks, lineage_file, indent=1) lineage_file.flush() @@ -1068,26 +1189,26 @@ def action_save_track(self): tracked_file.flush() trks.add(tracked_file.name, "tracked.npy") try: - s3.upload_file(file, self.output_bucket, self.subfolders) + # go to beginning of file object + trk_file_obj.seek(0) + s3 = self._get_s3_client() + s3.upload_fileobj(trk_file_obj, self.output_bucket, self.subfolders) except Exception as e: print("Something Happened: ", e, file=sys.stderr) raise - #os.remove(file) - return "Success!" - def add_cell_info(self, add_label, frame): ''' helper function for actions that add a cell to the trk ''' - #if cell already exists elsewhere in trk: + # if cell already exists elsewhere in trk: try: old_frames = self.tracks[add_label]['frames'] updated_frames = np.append(old_frames, frame) updated_frames = np.unique(updated_frames).tolist() self.tracks[add_label].update({'frames': updated_frames}) - #cell does not exist anywhere in trk: + # cell does not exist anywhere in trk: except KeyError: self.tracks.update({add_label: {}}) self.tracks[add_label].update({'label': int(add_label)}) @@ -1097,18 +1218,18 @@ def add_cell_info(self, add_label, frame): self.tracks[add_label].update({'parent': None}) self.tracks[add_label].update({'capped': False}) - self.frames_changed = self.info_changed = True + self._y_changed = self.info_changed = True def del_cell_info(self, del_label, frame): ''' helper function for actions that remove a cell from the trk ''' - #remove cell from frame + # remove cell from frame old_frames = self.tracks[del_label]['frames'] updated_frames = np.delete(old_frames, np.where(old_frames == np.int64(frame))).tolist() self.tracks[del_label].update({'frames': updated_frames}) - #if that was the last frame, delete the entry for that cell + # if that was the last frame, delete the entry for that cell if self.tracks[del_label]['frames'] == []: del self.tracks[del_label] @@ -1121,14 +1242,14 @@ def del_cell_info(self, del_label, frame): if track["parent"] == del_label: track["parent"] = None - self.frames_changed = self.info_changed = True + self._y_changed = self.info_changed = True def consecutive(data, stepsize=1): - return np.split(data, np.where(np.diff(data) != stepsize)[0]+1) + return np.split(data, np.where(np.diff(data) != stepsize)[0] + 1) -def predict_zstack_cell_ids(img, next_img, threshold = 0.1): +def predict_zstack_cell_ids(img, next_img, threshold=0.1): ''' Predict labels for next_img based on intersection over union (iou) with img. If cells don't meet threshold for iou, they don't count as @@ -1141,66 +1262,66 @@ def predict_zstack_cell_ids(img, next_img, threshold = 0.1): # relabel to remove skipped values, keeps subsequent predictions cleaner next_img = relabel_frame(next_img) - #create np array that can hold all pairings between cells in one - #image and cells in next image - iou = np.zeros((np.max(img)+1, np.max(next_img)+1)) + # create np array that can hold all pairings between cells in one + # image and cells in next image + iou = np.zeros((np.max(img) + 1, np.max(next_img) + 1)) vals = np.unique(img) cells = vals[np.nonzero(vals)] - #nothing to predict off of + # nothing to predict off of if len(cells) == 0: return next_img next_vals = np.unique(next_img) next_cells = next_vals[np.nonzero(next_vals)] - #no values to reassign + # no values to reassign if len(next_cells) == 0: return next_img - #calculate IOUs + # calculate IOUs for i in cells: for j in next_cells: - intersection = np.logical_and(img==i,next_img==j) - union = np.logical_or(img==i,next_img==j) - iou[i,j] = intersection.sum(axis=(0,1)) / union.sum(axis=(0,1)) + intersection = np.logical_and(img == i, next_img == j) + union = np.logical_or(img == i, next_img == j) + iou[i, j] = intersection.sum(axis=(0, 1)) / union.sum(axis=(0, 1)) - #relabel cells appropriately + # relabel cells appropriately - #relabeled_next holds cells as they get relabeled appropriately - relabeled_next = np.zeros(next_img.shape, dtype = np.uint16) + # relabeled_next holds cells as they get relabeled appropriately + relabeled_next = np.zeros(next_img.shape, dtype=np.uint16) - #max_indices[cell_from_next_img] -> cell from first image that matches it best - max_indices = np.argmax(iou, axis = 0) + # max_indices[cell_from_next_img] -> cell from first image that matches it best + max_indices = np.argmax(iou, axis=0) - #put cells that into new image if they've been matched with another cell + # put cells that into new image if they've been matched with another cell - #keep track of which (next_img)cells don't have matches - #this can be if (next_img)cell matched background, or if (next_img)cell matched - #a cell already used + # keep track of which (next_img)cells don't have matches + # this can be if (next_img)cell matched background, or if (next_img)cell matched + # a cell already used unmatched_cells = [] - #don't reuse cells (if multiple cells in next_img match one particular cell) + # don't reuse cells (if multiple cells in next_img match one particular cell) used_cells_src = [] - #next_cell ranges between 0 and max(next_img) - #matched_cell is which cell in img matched next_cell the best + # next_cell ranges between 0 and max(next_img) + # matched_cell is which cell in img matched next_cell the best # this for loop does the matching between cells for next_cell, matched_cell in enumerate(max_indices): - #if more than one match, look for best match - #otherwise the first match gets linked together, not necessarily reproducible + # if more than one match, look for best match + # otherwise the first match gets linked together, not necessarily reproducible # matched_cell != 0 prevents adding the background to used_cells_src if matched_cell != 0 and matched_cell not in used_cells_src: bool_matches = np.where(max_indices == matched_cell) count_matches = np.count_nonzero(bool_matches) if count_matches > 1: - #for a given cell in img, which next_cell has highest iou - matching_next_options = np.argmax(iou, axis =1) + # for a given cell in img, which next_cell has highest iou + matching_next_options = np.argmax(iou, axis=1) best_matched_next = matching_next_options[matched_cell] - #ignore if best_matched_next is the background + # ignore if best_matched_next is the background if best_matched_next != 0: if next_cell != best_matched_next: unmatched_cells = np.append(unmatched_cells, next_cell) @@ -1208,9 +1329,11 @@ def predict_zstack_cell_ids(img, next_img, threshold = 0.1): else: # don't add if bad match if iou[matched_cell][best_matched_next] > threshold: - relabeled_next = np.where(next_img == best_matched_next, matched_cell, relabeled_next) + relabeled_next = np.where(next_img == best_matched_next, + matched_cell, relabeled_next) - # if it's a bad match, we still need to add next_cell back into relabeled next later + # if it's a bad match, we still need to add next_cell back + # into relabeled next later elif iou[matched_cell][best_matched_next] <= threshold: unmatched_cells = np.append(unmatched_cells, best_matched_next) @@ -1219,7 +1342,7 @@ def predict_zstack_cell_ids(img, next_img, threshold = 0.1): # matched_cell != 0 is still true elif count_matches == 1: - #add the matched cell to the relabeled image + # add the matched cell to the relabeled image if iou[matched_cell][next_cell] > threshold: relabeled_next = np.where(next_img == next_cell, matched_cell, relabeled_next) else: @@ -1228,22 +1351,23 @@ def predict_zstack_cell_ids(img, next_img, threshold = 0.1): used_cells_src = np.append(used_cells_src, matched_cell) elif matched_cell in used_cells_src and next_cell != 0: - #skip that pairing, add next_cell to unmatched_cells + # skip that pairing, add next_cell to unmatched_cells unmatched_cells = np.append(unmatched_cells, next_cell) - #if the cell in next_img didn't match anything (and is not the background): - if matched_cell == 0 and next_cell !=0: + # if the cell in next_img didn't match anything (and is not the background): + if matched_cell == 0 and next_cell != 0: unmatched_cells = np.append(unmatched_cells, next_cell) - #note: this also puts skipped (nonexistent) labels into unmatched cells, main reason to relabel first + # note: this also puts skipped (nonexistent) labels into unmatched cells, + # main reason to relabel first - #figure out which labels we should use to label remaining, unmatched cells + # figure out which labels we should use to label remaining, unmatched cells - #these are the values that have already been used in relabeled_next + # these are the values that have already been used in relabeled_next relabeled_values = np.unique(relabeled_next)[np.nonzero(np.unique(relabeled_next))] - #to account for any new cells that appear, create labels by adding to the max number of cells - #assumes that these are new cells and that all prev labels have been assigned - #only make as many new labels as needed + # to account for any new cells that appear, create labels by adding to the max number of cells + # assumes that these are new cells and that all prev labels have been assigned + # only make as many new labels as needed current_max = max(np.max(cells), np.max(relabeled_values)) + 1 @@ -1252,26 +1376,26 @@ def predict_zstack_cell_ids(img, next_img, threshold = 0.1): stringent_allowed.append(current_max) current_max += 1 - #replace each unmatched cell with a value from the stringent_allowed list, - #add that relabeled cell to relabeled_next + # replace each unmatched cell with a value from the stringent_allowed list, + # add that relabeled cell to relabeled_next if len(unmatched_cells) > 0: for reassigned_cell in range(len(unmatched_cells)): relabeled_next = np.where(next_img == unmatched_cells[reassigned_cell], - stringent_allowed[reassigned_cell], relabeled_next) + stringent_allowed[reassigned_cell], relabeled_next) return relabeled_next -def relabel_frame(img, start_val = 1): +def relabel_frame(img, start_val=1): '''relabel cells in frame starting from 1 without skipping values''' - #cells in image to be relabeled + # cells in image to be relabeled cell_list = np.unique(img) cell_list = cell_list[np.nonzero(cell_list)] - relabeled_cell_list = range(start_val, len(cell_list)+start_val) + relabeled_cell_list = range(start_val, len(cell_list) + start_val) - relabeled_img = np.zeros(img.shape, dtype = np.uint16) + relabeled_img = np.zeros(img.shape, dtype=np.uint16) for i, cell in enumerate(cell_list): relabeled_img = np.where(img == cell, relabeled_cell_list[i], relabeled_img) @@ -1280,22 +1404,27 @@ def relabel_frame(img, start_val = 1): def load_npz(filename): - data = BytesIO(filename) + data = io.BytesIO(filename) npz = np.load(data) + # standard nomenclature for image (X) and annotation (y) if 'y' in npz.files: raw_stack = npz['X'] annotation_stack = npz['y'] + # some files may have alternate names 'raw' and 'annotated' elif 'raw' in npz.files: raw_stack = npz['raw'] annotation_stack = npz['annotated'] + + # if files are named something different, give it a try anyway else: raw_stack = npz[npz.files[0]] annotation_stack = npz[npz.files[1]] return {"raw": raw_stack, "annotated": annotation_stack} + # copied from: # vanvalenlab/deepcell-tf/blob/master/deepcell/utils/tracking_utils.py3 def load_trks(trkfile): @@ -1310,13 +1439,13 @@ def load_trks(trkfile): with tarfile.open(temp.name, 'r') as trks: # numpy can't read these from disk... - array_file = BytesIO() + array_file = io.BytesIO() array_file.write(trks.extractfile('raw.npy').read()) array_file.seek(0) raw = np.load(array_file) array_file.close() - array_file = BytesIO() + array_file = io.BytesIO() array_file.write(trks.extractfile('tracked.npy').read()) array_file.seek(0) tracked = np.load(array_file) diff --git a/browser/config.py b/browser/config.py index cfff3c7a2..425863f90 100644 --- a/browser/config.py +++ b/browser/config.py @@ -1,16 +1,29 @@ -import os -import sys +"""Configuration options and environment variables.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function from decouple import config -DEBUG = True -PORT = 5000 -S3_KEY = config('S3_KEY') -S3_SECRET = config('S3_SECRET') +DEBUG = config('DEBUG', cast=bool, default=True) +PORT = config('PORT', cast=int, default=5000) -MYSQL_USERNAME = config('MYSQL_USERNAME') -MYSQL_HOSTNAME = config('MYSQL_HOSTNAME') -MYSQL_PORT = config('MYSQL_PORT', cast = int, default = 3306) -MYSQL_PASSWORD = config('MYSQL_PASSWORD') -MYSQL_DATABASE = config('MYSQL_DATABASE', default = 'caliban') +AWS_ACCESS_KEY_ID = config('AWS_ACCESS_KEY_ID', default='') +AWS_SECRET_ACCESS_KEY = config('AWS_SECRET_ACCESS_KEY', default='') + +TEMPLATES_AUTO_RELOAD = config('TEMPLATES_AUTO_RELOAD', cast=bool, default=True) + +# SQLAlchemy settings +SQLALCHEMY_TRACK_MODIFICATIONS = config('SQLALCHEMY_TRACK_MODIFICATIONS', + cast=bool, default=False) + +SQLALCHEMY_DATABASE_URI = config('SQLALCHEMY_DATABASE_URI', + default='sqlite:////tmp/caliban.db') + +# Compression settings +COMPRESS_MIMETYPES = ['text/html', 'text/css', 'text/xml', + 'application/json', 'application/javascript'] +COMPRESS_LEVEL = 6 +COMPRESS_MIN_SIZE = 500 +COMPRESS_ALGORITHM = 'gzip' diff --git a/browser/conftest.py b/browser/conftest.py new file mode 100644 index 000000000..e610bc82c --- /dev/null +++ b/browser/conftest.py @@ -0,0 +1,42 @@ +"""Tests for the Caliban Flask App.""" + +import os + +from flask_sqlalchemy import SQLAlchemy + +import pytest + +from application import create_app # pylint: disable=C0413 + +# flask-sqlalchemy fixtures from http://alexmic.net/flask-sqlalchemy-pytest/ + + +TESTDB_PATH = '/tmp/test_project.db' +TEST_DATABASE_URI = 'sqlite:///{}'.format(TESTDB_PATH) + + +@pytest.fixture +def app(): + """Session-wide test `Flask` application.""" + + if os.path.exists(TESTDB_PATH): + os.unlink(TESTDB_PATH) + + yield create_app( + TESTING=True, + SQLALCHEMY_DATABASE_URI=TEST_DATABASE_URI, + ) + + os.unlink(TESTDB_PATH) + + +@pytest.fixture +def _db(app): + """ + Provide the transactional fixtures with access to the database via a Flask-SQLAlchemy + database connection. + + https://pypi.org/project/pytest-flask-sqlalchemy/ + """ + db = SQLAlchemy(app=app) + return db diff --git a/browser/docker-compose.yaml b/browser/docker-compose.yaml new file mode 100644 index 000000000..f6a77ff16 --- /dev/null +++ b/browser/docker-compose.yaml @@ -0,0 +1,25 @@ +version: "2" +services: + app: + build: . + links: + - db + ports: + - 5000:5000 + depends_on: + - db + environment: + AWS_ACCESS_KEY_ID: '' + AWS_SECRET_ACCESS_KEY: '' + SQLALCHEMY_DATABASE_URI: mysql://root:password@db:3306/caliban + db: + image: mysql:8.0 + command: mysqld --default-authentication-plugin=mysql_native_password + ports: + - 3306:3306 + environment: + MYSQL_PASSWORD: password + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: caliban + volumes: + - ./db:/docker-entrypoint-initdb.d/:rw diff --git a/browser/eb_application.py b/browser/eb_application.py deleted file mode 100644 index b7e4fee60..000000000 --- a/browser/eb_application.py +++ /dev/null @@ -1,357 +0,0 @@ -"""Flask app route handlers""" - -import base64 -import json -import os -import pickle -import re -import sys -import traceback - -import MySQLdb - -from flask import Flask, jsonify, render_template, request, redirect - -from helpers import is_trk_file, is_npz_file -from caliban import TrackReview, ZStackReview -import config - -# Create and configure the app -application = Flask(__name__) # pylint: disable=C0103 -app = application -app.config.from_object("config") - - -@app.route("/upload_file/", methods=["GET", "POST"]) -def upload_file(project_id): - ''' Upload .trk/.npz data file to AWS S3 bucket. - ''' - conn = create_connection("caliban.db") - # Use id to grab appropriate TrackReview/ZStackReview object from database - id_exists = get_project(conn, project_id) - - if id_exists is None: - conn.close() - return jsonify({'error': 'project_id not found'}), 404 - - state = pickle.loads(id_exists[2]) - - # Call function in caliban.py to save data file and send to S3 bucket - if is_trk_file(id_exists[1]): - state.action_save_track() - elif is_npz_file(id_exists[1]): - state.action_save_zstack() - - # Delete id and object from database - delete_project(conn, project_id) - conn.close() - - return redirect("/") - - -@app.route("/action///", methods=["POST"]) -def action(project_id, action_type, frame): - ''' Make an edit operation to the data file and update the object - in the database. - ''' - - # obtain 'info' parameter data sent by .js script - info = {k: json.loads(v) for k, v in request.values.to_dict().items()} - frame = int(frame) - - try: - conn = create_connection("caliban.db") - # Use id to grab appropriate TrackReview/ZStackReview object from database - id_exists = get_project(conn, project_id) - - if id_exists is None: - conn.close() - return jsonify({'error': 'project_id not found'}), 404 - - state = pickle.loads(id_exists[2]) - # Perform edit operation on the data file - state.action(action_type, info) - frames_changed = state.frames_changed - info_changed = state.info_changed - - state.frames_changed = state.info_changed = False - - # Update object in local database - update_object(conn, (id_exists[1], state, project_id)) - conn.close() - - except Exception as e: - traceback.print_exc() - return jsonify({"error": str(e)}), 500 - - if info_changed: - tracks = state.readable_tracks - else: - tracks = False - - if frames_changed: - img = state.get_frame(frame, raw=False) - raw = state.get_frame(frame, raw=True) - edit_arr = state.get_array(frame) - - encode = lambda x: base64.encodebytes(x.read()).decode() - - img_payload = { - 'raw': f'data:image/png;base64,{encode(raw)}', - 'segmented': f'data:image/png;base64,{encode(img)}', - 'seg_arr': edit_arr.tolist() - } - else: - img_payload = False - - return jsonify({"tracks": tracks, "imgs": img_payload}) - -@app.route("/frame//") -def get_frame(frame, project_id): - ''' Serve modes of frames as pngs. Send pngs and color mappings of - cells to .js file. - ''' - frame = int(frame) - conn = create_connection("caliban.db") - # Use id to grab appropriate TrackReview/ZStackReview object from database - id_exists = get_project(conn, project_id) - conn.close() - - if id_exists is None: - return jsonify({'error': 'project_id not found'}), 404 - - state = pickle.loads(id_exists[2]) - - # Obtain raw, mask, and edit mode frames - img = state.get_frame(frame, raw=False) - raw = state.get_frame(frame, raw=True) - - # Obtain color map of the cells - edit_arr = state.get_array(frame) - - encode = lambda x: base64.encodebytes(x.read()).decode() - - payload = { - 'raw': f'data:image/png;base64,{encode(raw)}', - 'segmented': f'data:image/png;base64,{encode(img)}', - 'seg_arr': edit_arr.tolist() - } - - return jsonify(payload) - -@app.route("/load/", methods=["POST"]) -def load(filename): - ''' Initate TrackReview/ZStackReview object and load object to database. - Send specific attributes of the object to the .js file. - ''' - conn = create_connection("caliban.db") - - print(f"Loading track at {filename}", file=sys.stderr) - - folders = re.split('__', filename) - filename = folders[len(folders) - 1] - subfolders = folders[2:len(folders)] - - subfolders = '/'.join(subfolders) - - input_bucket = folders[0] - output_bucket = folders[1] - - if is_trk_file(filename): - # Initate TrackReview object and entry in database - track_review = TrackReview(filename, input_bucket, output_bucket, subfolders) - project_id = create_project(conn, filename, track_review) - conn.commit() - conn.close() - - # Send attributes to .js file - return jsonify({ - "max_frames": track_review.max_frames, - "tracks": track_review.readable_tracks, - "dimensions": track_review.dimensions, - "project_id": project_id, - "screen_scale": track_review.scale_factor - }) - - if is_npz_file(filename): - # Initate ZStackReview object and entry in database - zstack_review = ZStackReview(filename, input_bucket, output_bucket, subfolders) - project_id = create_project(conn, filename, zstack_review) - conn.commit() - conn.close() - - # Send attributes to .js file - return jsonify({ - "max_frames": zstack_review.max_frames, - "channel_max": zstack_review.channel_max, - "feature_max": zstack_review.feature_max, - "tracks": zstack_review.readable_tracks, - "dimensions": zstack_review.dimensions, - "project_id": project_id, - "screen_scale": zstack_review.scale_factor - }) - - conn.close() - error = { - 'error': 'invalid file extension: {}'.format( - os.path.splitext(filename)[-1]) - } - return jsonify(error), 400 - - -@app.route('/', methods=['GET', 'POST']) -def form(): - ''' Request HTML landing page to be rendered if user requests for - http://127.0.0.1:5000/. - ''' - return render_template('form.html') - - -@app.route('/tool', methods=['GET', 'POST']) -def tool(): - ''' Request HTML caliban tool page to be rendered after user inputs - filename in the landing page. - ''' - filename = request.form['filename'] - print(f"{filename} is filename", file=sys.stderr) - - new_filename = 'caliban-input__caliban-output__test__{}'.format( - str(filename)) - - if is_trk_file(new_filename): - return render_template('index_track.html', filename=new_filename) - if is_npz_file(new_filename): - return render_template('index_zstack.html', filename=new_filename) - - error = { - 'error': 'invalid file extension: {}'.format( - os.path.splitext(filename)[-1]) - } - return jsonify(error), 400 - - -@app.route('/', methods=['GET', 'POST']) -def shortcut(filename): - ''' Request HTML caliban tool page to be rendered if user makes a URL - request to access a specific data file that has been preloaded to the - input S3 bucket (ex. http://127.0.0.1:5000/test.npz). - ''' - - if is_trk_file(filename): - return render_template('index_track.html', filename=filename) - if is_npz_file(filename): - return render_template('index_zstack.html', filename=filename) - - error = { - 'error': 'invalid file extension: {}'.format( - os.path.splitext(filename)[-1]) - } - return jsonify(error), 400 - -def create_connection(_): - ''' Create a database connection to a SQLite database. - ''' - conn = None - try: - conn = MySQLdb.connect( - user=config.MYSQL_USERNAME, - host=config.MYSQL_HOSTNAME, - port=config.MYSQL_PORT, - passwd=config.MYSQL_PASSWORD, - db=config.MYSQL_DATABASE, - charset='utf8', - use_unicode=True) - except MySQLdb._exceptions.MySQLError as err: - print(err) - return conn - - -def create_table(conn, create_table_sql): - ''' Create a table from the create_table_sql statement. - ''' - try: - cursor = conn.cursor() - cursor.execute(create_table_sql) - except MySQLdb._exceptions.MySQLError as err: - print(err) - - -def create_project(conn, filename, data): - ''' Create a new project in the database table. - ''' - sql = ''' INSERT INTO projects(filename, state) - VALUES(%s, %s) ''' - cursor = conn.cursor() - - # convert object to binary data to be stored as data type BLOB - state_data = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) - - cursor.execute(sql, (filename, state_data)) - return cursor.lastrowid - - -def update_object(conn, project): - ''' Update filename, state of a project. - ''' - sql = ''' UPDATE projects - SET filename = %s , - state = %s - WHERE id = %s''' - - # convert object to binary data to be stored as data type BLOB - state_data = pickle.dumps(project[1], pickle.HIGHEST_PROTOCOL) - - cur = conn.cursor() - cur.execute(sql, (project[0], state_data, project[2])) - conn.commit() - - -def get_project(conn, project_id): - '''Fetches TrackReview/ZStackReview object from database by project_id. - - Args: - conn (obj): SQL database connection. - project_id (int): The primary key of the projects table. - - Returns: - tuple: all data columns matching the project_id. - ''' - cur = conn.cursor() - cur.execute("SELECT * FROM {tn} WHERE {idf}={my_id}".format( - tn="projects", - idf="id", - my_id=project_id - )) - return cur.fetchone() - - -def delete_project(conn, project_id): - ''' Delete data object (TrackReview/ZStackReview) by id. - ''' - sql = 'DELETE FROM projects WHERE id=%s' - cur = conn.cursor() - cur.execute(sql, (project_id,)) - conn.commit() - - -def main(): - ''' Runs app and initiates database file if it doesn't exist. - ''' - conn = create_connection("caliban.db") - sql_create_projects_table = """ - CREATE TABLE IF NOT EXISTS projects ( - id integer NOT NULL AUTO_INCREMENT PRIMARY KEY, - filename text NOT NULL, - state longblob NOT NULL - ); - """ - create_table(conn, sql_create_projects_table) - conn.commit() - conn.close() - - app.jinja_env.auto_reload = True - app.config['TEMPLATES_AUTO_RELOAD'] = True - app.run('0.0.0.0', port=5000) - -if __name__ == "__main__": - main() diff --git a/browser/env.example b/browser/env.example deleted file mode 100644 index a1e5fc325..000000000 --- a/browser/env.example +++ /dev/null @@ -1,10 +0,0 @@ -# AWS credentials -S3_KEY= -S3_SECRET= - -# MySQL database credentials -MYSQL_USERNAME= -MYSQL_HOSTNAME= -MYSQL_PORT= -MYSQL_PASSWORD= -MYSQL_DATABASE= \ No newline at end of file diff --git a/browser/helpers.py b/browser/helpers.py index 60f65d481..9f2b0b4c1 100644 --- a/browser/helpers.py +++ b/browser/helpers.py @@ -1,10 +1,12 @@ import os + ALLOWED_EXTENSIONS = set([ '.txt', '.md', '.markdown', '.pdf', '.png', '.jpg', '.jpeg', '.gif', ]) + def allowed_file(name): return os.path.splitext(str(name).lower())[-1] in ALLOWED_EXTENSIONS diff --git a/browser/helpers_test.py b/browser/helpers_test.py new file mode 100644 index 000000000..9196cf327 --- /dev/null +++ b/browser/helpers_test.py @@ -0,0 +1,42 @@ +"""Tests for helpers.py""" + +import pytest + +import helpers + + +def test_allowed_file(mocker): + extensions = set(['.png']) + mocker.patch('helpers.ALLOWED_EXTENSIONS', extensions) + + assert helpers.allowed_file('test.png') + assert helpers.allowed_file('test.PnG') + assert not helpers.allowed_file('test.pdf') + assert not helpers.allowed_file('this is just a string') + assert not helpers.allowed_file(1234) + assert not helpers.allowed_file(None) + assert not helpers.allowed_file(dict()) + + +def test_is_trk_file(): + assert helpers.is_trk_file('test.trk') + assert helpers.is_trk_file('test.trks') + assert helpers.is_trk_file('test.TrKs') + assert helpers.is_trk_file('test.TRk') + assert not helpers.is_trk_file('test.pdf') + assert not helpers.is_trk_file('test.npz') + assert not helpers.is_trk_file('this is just a string') + assert not helpers.is_trk_file(1234) + assert not helpers.is_trk_file(None) + assert not helpers.is_trk_file(dict()) + + +def test_is_npz_file(): + assert helpers.is_npz_file('test.npz') + assert helpers.is_npz_file('test.NpZ') + assert not helpers.is_npz_file('test.pdf') + assert not helpers.is_npz_file('test.trk') + assert not helpers.is_npz_file('this is just a string') + assert not helpers.is_npz_file(1234) + assert not helpers.is_npz_file(None) + assert not helpers.is_npz_file(dict()) diff --git a/browser/imgutils.py b/browser/imgutils.py index a9cfbdc78..7a511988c 100644 --- a/browser/imgutils.py +++ b/browser/imgutils.py @@ -1,13 +1,22 @@ +"""Utilities for handling images""" import io + import matplotlib.pyplot as plt +from matplotlib.colors import Normalize + +from PIL import Image -def pngify(imgarr, vmin, vmax, cmap): +def pngify(imgarr, vmin, vmax, cmap=None): out = io.BytesIO() - plt.imsave(out, imgarr, - vmin=vmin, - vmax=vmax, - cmap=cmap, - format="png") + + if cmap: + cmap = plt.get_cmap(cmap) + imgarr = Normalize(vmin=vmin, vmax=vmax)(imgarr) + # apply the colormap + imgarr = cmap(imgarr, bytes=True) + + img = Image.fromarray(imgarr) + img.save(out, format="png") out.seek(0) return out diff --git a/browser/imgutils_test.py b/browser/imgutils_test.py new file mode 100644 index 000000000..1cb1b1d67 --- /dev/null +++ b/browser/imgutils_test.py @@ -0,0 +1,42 @@ +"""Tests for helpers.py""" + +import os + +from skimage.io import imread +import numpy as np +import matplotlib.pyplot as plt + +import pytest + +import imgutils + + +def test_pngify(tmpdir): + outfile = os.path.join(str(tmpdir), 'output.png') + imgarr = np.random.randint(0, 255, size=(32, 32), dtype='uint16') + + # test vmin, vmax, and cmap all None + out = imgutils.pngify(imgarr, None, None, cmap=None) + with open(outfile, 'wb') as f: + f.write(out.getbuffer()) + + loaded_image = np.uint16(imread(outfile)) + np.testing.assert_equal(imgarr, loaded_image) + + # test vmin, vmax + out = imgutils.pngify(imgarr, 0, imgarr.max(), cmap=None) + with open(outfile, 'wb') as f: + f.write(out.getbuffer()) + + loaded_image = np.uint16(imread(outfile)) + np.testing.assert_equal(imgarr, loaded_image) + + # test vmin, vmax and cmap + cmap = 'cubehelix' + out = imgutils.pngify(imgarr, 0, imgarr.max(), cmap=cmap) + with open(outfile, 'wb') as f: + f.write(out.getbuffer()) + + loaded_image = np.uint16(imread(outfile)) + print(imgarr.shape, loaded_image.shape) + np.testing.assert_equal(imgarr.shape, loaded_image.shape[:-1]) diff --git a/browser/models.py b/browser/models.py new file mode 100644 index 000000000..96b61707e --- /dev/null +++ b/browser/models.py @@ -0,0 +1,85 @@ +"""SQL Alchemy database models.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import logging +import pickle +import timeit + +from flask_sqlalchemy import SQLAlchemy + + +logger = logging.getLogger('models.Project') # pylint: disable=C0103 +db = SQLAlchemy() # pylint: disable=C0103 + + +class Project(db.Model): + """Project table definition.""" + # pylint: disable=E1101 + __tablename__ = 'projects' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + filename = db.Column(db.Text, nullable=False) + state = db.Column(db.LargeBinary(length=(2 ** 32) - 1)) + subfolders = db.Column(db.Text, nullable=False) + createdAt = db.Column(db.TIMESTAMP, nullable=False, default=db.func.now()) + updatedAt = db.Column(db.TIMESTAMP, nullable=False, default=db.func.now(), + onupdate=db.func.current_timestamp()) + finished = db.Column(db.TIMESTAMP) + numUpdates = db.Column(db.Integer, nullable=False, default=0) + firstUpdate = db.Column(db.TIMESTAMP) + lastUpdate = db.Column(db.TIMESTAMP) + + def __init__(self, filename, state, subfolders): + self.filename = filename + self.state = state + self.subfolders = subfolders + + @staticmethod + def get_project_by_id(project_id): + """Return the project with the given ID, if it exists.""" + start = timeit.default_timer() + project = Project.query.filter_by(id=project_id).first() + logger.debug('Got project with ID = "%s" in %ss.', + project_id, timeit.default_timer() - start) + return project + + @staticmethod + def create_project(filename, state, subfolders): + """Create a new project.""" + start = timeit.default_timer() + state_data = pickle.dumps(state, pickle.HIGHEST_PROTOCOL) + new_project = Project( + filename=filename, + state=state_data, + subfolders=subfolders) + db.session.add(new_project) + db.session.commit() + logger.debug('Created new project with ID = "%s" in %ss.', + new_project.id, timeit.default_timer() - start) + return new_project + + @staticmethod + def update_project(project, state): + """Update a project's current state.""" + start = timeit.default_timer() + if not project.firstUpdate: + project.firstUpdate = db.func.current_timestamp() + + project.state = pickle.dumps(state, pickle.HIGHEST_PROTOCOL) + project.numUpdates += 1 + + db.session.commit() + logger.debug('Updated project with ID = "%s" in %ss.', + project.id, timeit.default_timer() - start) + + @staticmethod + def finish_project(project): + """Complete a project and set the state to null.""" + start = timeit.default_timer() + project.lastUpdate = project.updatedAt + project.finished = db.func.current_timestamp() + project.state = None + db.session.commit() # commit the changes + logger.debug('Finished project with ID = "%s" in %ss.', + project.id, timeit.default_timer() - start) diff --git a/browser/models_test.py b/browser/models_test.py new file mode 100644 index 000000000..323d9226a --- /dev/null +++ b/browser/models_test.py @@ -0,0 +1,49 @@ +"""Test for Caliban Models""" + +import pickle + +import pytest + +# from flask_sqlalchemy import SQLAlchemy + +import models + + +def test_project(db_session): + # TODO: is there a good way to separate these tests into unit tests? + + # test that no projects exist + project = models.Project.get_project_by_id(1) + assert project is None + + # test create project + filename = 'filename' + state = b'state_data' + subfolders = 'subfolders' + + project = models.Project.create_project( + filename=filename, + state=state, + subfolders=subfolders) + + valid_id = models.Project.id + + # test that the project can be found and is the same as the created one + found_project = models.Project.get_project_by_id(valid_id) + assert found_project == project + + # test project is updated + new_state = b'updated state data' + models.Project.update_project(project, new_state) + + # get the updated project and make sure the data is updated. + found_project = models.Project.get_project_by_id(valid_id) + pickled_state = pickle.dumps(new_state, pickle.HIGHEST_PROTOCOL) + assert found_project.state == pickled_state + + # test project is finished + models.Project.finish_project(project) + found_project = models.Project.get_project_by_id(valid_id) + assert found_project.state is None + assert found_project.finished is not None + assert found_project.lastUpdate is not None diff --git a/browser/requirements-test.txt b/browser/requirements-test.txt new file mode 100644 index 000000000..9cd005a5c --- /dev/null +++ b/browser/requirements-test.txt @@ -0,0 +1,9 @@ +pytest>=5.2 +pytest-cov==2.5.1 +pytest-mock +pytest-pep8 +pytest-flask +pytest-flask-sqlalchemy +fakeredis +six>=1.12 +coveralls diff --git a/browser/requirements.txt b/browser/requirements.txt index eb2e5ec02..7ecd6e7ef 100644 --- a/browser/requirements.txt +++ b/browser/requirements.txt @@ -1,31 +1,11 @@ -awscli==1.16.259 -boto3==1.9.182 -botocore==1.12.182 -Click==7.0 -config==0.4.2 -cycler==0.10.0 -docutils==0.14 -Flask==1.0.3 -get==2019.4.13 -itsdangerous==1.1.0 -Jinja2==2.10.1 -jmespath==0.9.4 -jsonify==0.5 -kiwisolver==1.1.0 -MarkupSafe==1.1.1 -matplotlib==3.1.0 +flask==1.1.1 +flask-cors==3.0.8 +flask-sqlalchemy==2.4.1 +matplotlib==3.0.3 mysqlclient==1.4.5 -numpy==1.16.4 -pillow>=6.2.0 -post==2019.4.13 -public==2019.4.13 -pyparsing==2.4.0 -python-dateutil==2.8.0 +numpy>=1.16.4 +boto3==1.9.182 +scikit-image>=0.15.0,<0.17.0 python-decouple==3.1 -query-string==2019.4.13 -request==2019.4.13 -s3transfer==0.2.1 -six==1.12.0 -scikit-image==0.15.0 -urllib3==1.25.3 -Werkzeug==0.15.4 +pillow==6.2.2 +flask-compress==1.5.0 diff --git a/browser/static/css/footer.css b/browser/static/css/footer.css deleted file mode 100644 index f27d3c25b..000000000 --- a/browser/static/css/footer.css +++ /dev/null @@ -1,3 +0,0 @@ -.footer-text { - text-align: center; -} \ No newline at end of file diff --git a/browser/static/css/form.css b/browser/static/css/form.css deleted file mode 100644 index e9b4c1d5e..000000000 --- a/browser/static/css/form.css +++ /dev/null @@ -1,54 +0,0 @@ -#say { -} - -.say-label { - text-align: center; -} - -#caliban-form { - width: 30em; - margin-top: 3em; - text-align: center; - padding: 3em 5em 5em 5em; -} - -.file-name-input { - display: flex; - box-sizing: border-box; - flex-shrink: 0; - flex-direction: column; - align-items: center; -} - -.file-name-input > :not(:first-child){ - margin-top: 2em; -} - -/* selects all children of the caliban-form except the first child. */ -#caliban-form > :not(:first-child) { - margin-top: 2em; -} - -.say-label, #say { - display: block; - vertical-align: top; -} - -#container { - display: flex; - box-sizing: border-box; - min-height: 93vh; - flex-direction: column; - align-items: center; -} - -.call-action { - font-size: 1.3125rem; - color: rgba(0, 0, 0, 0.54); - font-weight: 500; - line-height: 1.16667em; -} - -.form-help { - text-align: center; -} \ No newline at end of file diff --git a/browser/static/css/infopane.css b/browser/static/css/infopane.css deleted file mode 100644 index 4f52137ba..000000000 --- a/browser/static/css/infopane.css +++ /dev/null @@ -1,23 +0,0 @@ -.accordion { - background-color: #eee; - color: #444; - cursor: pointer; - padding: 18px; - width: 100%; - border: none; - text-align: left; - outline: none; - font-size: 15px; - transition: 0.4s; - } - - .active, .accordion:hover { - background-color: #ccc; - } - - .panel { - padding: 0 18px; - display: none; - background-color: white; - overflow: hidden; - } \ No newline at end of file diff --git a/browser/static/css/main.css b/browser/static/css/main.css index e179e800c..4d3dfd6d8 100644 --- a/browser/static/css/main.css +++ b/browser/static/css/main.css @@ -1,98 +1,99 @@ -.frame-info { - padding: 1em; +/* START Materialize Sticky Footer */ +body { + display: flex; + flex-direction: column; + min-height: 100vh; } -.file-input { - padding: 1em; +main { + flex: 1 0 auto; } +/* END Materialize Sticky Footer */ -.frame-info * { - display: block; +/* START container width override */ +.container { + margin: 0 auto; + max-width: 1280px; + width: 90%; } - -.view_mode * { - display: block; +@media only screen and (min-width: 601px) { + .container { + width: 90%; + } } - -.cell-info-block { - margin: 1em 1em 1em 0em; - padding: 1em; - border-radius: 1em; - box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); +@media only screen and (min-width: 993px) { + .container { + width: 90%; + } } - -.image-state { - padding: 1em; +@media only screen and (min-width: 1201px) { + .container { + width: 90%; + } } +/* END container width override */ -.cell-info-block * { - display: block; +/* START navbar CSS */ +.nav-wrapper { + padding-left: 1em; + padding-right: 1em; } +/* END navbar CSS */ -#container { - margin: 0 auto; - margin-top: 100px; - display: flex; - box-sizing: border-box; - flex-shrink: 0; - flex-direction: row; - justify-content: center; - align-items: center; +/* START infopane CSS */ +.infopane { + border: 0; + margin-top: 0; } -#maintable { - width: 900px; - height: 500px; - /* outline: 1px dashed blue; */ +.infopane .active div.collapsible-header, +.infopane li div.collapsible-header:hover { + background-color: #ccc!important; + transition: 0.4s } -.changed-label { - text-align: center; +.infopane li .collapsible-body { + padding-top: 0; } - #logcol, #canvascol{ - vertical-align: top; - } +/* END infopane CSS */ -#logcol { - box-sizing: border-box; - border-radius: 1em; - width: 20em; +/* START log table CSS */ +.info-table tr td { + padding-bottom: 0.5em; + padding-top: 0.5em; + text-align: left; +} +.info-table tr td:nth-child(odd) { + width: 40%; } +.info-table tr td:nth-child(even) { + width: 60%; +} +/* END log table CSS */ #edit_brush_row, #edit_label_row, #edit_erase_row { - visibility: hidden; + visibility: hidden; } #frames, #slices { - width: 180px; - word-wrap: break-word; /*All browsers since IE 5.5+*/ - overflow-wrap: break-word; - max-height: 68px; - overflow: auto; + overflow: auto; + overflow-wrap: break-word; + word-wrap: break-word; /*All browsers since IE 5.5+*/ } #mode { - width: 180px; - word-wrap: break-word; - overflow-wrap: break-word; + overflow-wrap: break-word; + word-wrap: break-word; } #canvas { - cursor: crosshair; - box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); -} - -#hidden_canvas, #hidden_seg_canvas { - display: none; + background: black; + cursor: crosshair; } -#canvascol { - vertical-align: top; +#output { + display: block; + max-width: 100%; + overflow-wrap: break-word; + word-wrap: break-word; } - -#give_filename { - display: block; - word-wrap: break-word; - overflow-wrap: break-word; - max-width: 300px; -} \ No newline at end of file diff --git a/browser/static/css/materialize.min.css b/browser/static/css/materialize.min.css new file mode 100644 index 000000000..74b1741b6 --- /dev/null +++ b/browser/static/css/materialize.min.css @@ -0,0 +1,13 @@ +/*! + * Materialize v1.0.0 (http://materializecss.com) + * Copyright 2014-2017 Materialize + * MIT License (https://raw.githubusercontent.com/Dogfalo/materialize/master/LICENSE) + */ +.materialize-red{background-color:#e51c23 !important}.materialize-red-text{color:#e51c23 !important}.materialize-red.lighten-5{background-color:#fdeaeb !important}.materialize-red-text.text-lighten-5{color:#fdeaeb !important}.materialize-red.lighten-4{background-color:#f8c1c3 !important}.materialize-red-text.text-lighten-4{color:#f8c1c3 !important}.materialize-red.lighten-3{background-color:#f3989b !important}.materialize-red-text.text-lighten-3{color:#f3989b !important}.materialize-red.lighten-2{background-color:#ee6e73 !important}.materialize-red-text.text-lighten-2{color:#ee6e73 !important}.materialize-red.lighten-1{background-color:#ea454b !important}.materialize-red-text.text-lighten-1{color:#ea454b !important}.materialize-red.darken-1{background-color:#d0181e !important}.materialize-red-text.text-darken-1{color:#d0181e !important}.materialize-red.darken-2{background-color:#b9151b !important}.materialize-red-text.text-darken-2{color:#b9151b !important}.materialize-red.darken-3{background-color:#a21318 !important}.materialize-red-text.text-darken-3{color:#a21318 !important}.materialize-red.darken-4{background-color:#8b1014 !important}.materialize-red-text.text-darken-4{color:#8b1014 !important}.red{background-color:#F44336 !important}.red-text{color:#F44336 !important}.red.lighten-5{background-color:#FFEBEE !important}.red-text.text-lighten-5{color:#FFEBEE !important}.red.lighten-4{background-color:#FFCDD2 !important}.red-text.text-lighten-4{color:#FFCDD2 !important}.red.lighten-3{background-color:#EF9A9A !important}.red-text.text-lighten-3{color:#EF9A9A !important}.red.lighten-2{background-color:#E57373 !important}.red-text.text-lighten-2{color:#E57373 !important}.red.lighten-1{background-color:#EF5350 !important}.red-text.text-lighten-1{color:#EF5350 !important}.red.darken-1{background-color:#E53935 !important}.red-text.text-darken-1{color:#E53935 !important}.red.darken-2{background-color:#D32F2F !important}.red-text.text-darken-2{color:#D32F2F !important}.red.darken-3{background-color:#C62828 !important}.red-text.text-darken-3{color:#C62828 !important}.red.darken-4{background-color:#B71C1C !important}.red-text.text-darken-4{color:#B71C1C !important}.red.accent-1{background-color:#FF8A80 !important}.red-text.text-accent-1{color:#FF8A80 !important}.red.accent-2{background-color:#FF5252 !important}.red-text.text-accent-2{color:#FF5252 !important}.red.accent-3{background-color:#FF1744 !important}.red-text.text-accent-3{color:#FF1744 !important}.red.accent-4{background-color:#D50000 !important}.red-text.text-accent-4{color:#D50000 !important}.pink{background-color:#e91e63 !important}.pink-text{color:#e91e63 !important}.pink.lighten-5{background-color:#fce4ec !important}.pink-text.text-lighten-5{color:#fce4ec !important}.pink.lighten-4{background-color:#f8bbd0 !important}.pink-text.text-lighten-4{color:#f8bbd0 !important}.pink.lighten-3{background-color:#f48fb1 !important}.pink-text.text-lighten-3{color:#f48fb1 !important}.pink.lighten-2{background-color:#f06292 !important}.pink-text.text-lighten-2{color:#f06292 !important}.pink.lighten-1{background-color:#ec407a !important}.pink-text.text-lighten-1{color:#ec407a !important}.pink.darken-1{background-color:#d81b60 !important}.pink-text.text-darken-1{color:#d81b60 !important}.pink.darken-2{background-color:#c2185b !important}.pink-text.text-darken-2{color:#c2185b !important}.pink.darken-3{background-color:#ad1457 !important}.pink-text.text-darken-3{color:#ad1457 !important}.pink.darken-4{background-color:#880e4f !important}.pink-text.text-darken-4{color:#880e4f !important}.pink.accent-1{background-color:#ff80ab !important}.pink-text.text-accent-1{color:#ff80ab !important}.pink.accent-2{background-color:#ff4081 !important}.pink-text.text-accent-2{color:#ff4081 !important}.pink.accent-3{background-color:#f50057 !important}.pink-text.text-accent-3{color:#f50057 !important}.pink.accent-4{background-color:#c51162 !important}.pink-text.text-accent-4{color:#c51162 !important}.purple{background-color:#9c27b0 !important}.purple-text{color:#9c27b0 !important}.purple.lighten-5{background-color:#f3e5f5 !important}.purple-text.text-lighten-5{color:#f3e5f5 !important}.purple.lighten-4{background-color:#e1bee7 !important}.purple-text.text-lighten-4{color:#e1bee7 !important}.purple.lighten-3{background-color:#ce93d8 !important}.purple-text.text-lighten-3{color:#ce93d8 !important}.purple.lighten-2{background-color:#ba68c8 !important}.purple-text.text-lighten-2{color:#ba68c8 !important}.purple.lighten-1{background-color:#ab47bc !important}.purple-text.text-lighten-1{color:#ab47bc !important}.purple.darken-1{background-color:#8e24aa !important}.purple-text.text-darken-1{color:#8e24aa !important}.purple.darken-2{background-color:#7b1fa2 !important}.purple-text.text-darken-2{color:#7b1fa2 !important}.purple.darken-3{background-color:#6a1b9a !important}.purple-text.text-darken-3{color:#6a1b9a !important}.purple.darken-4{background-color:#4a148c !important}.purple-text.text-darken-4{color:#4a148c !important}.purple.accent-1{background-color:#ea80fc !important}.purple-text.text-accent-1{color:#ea80fc !important}.purple.accent-2{background-color:#e040fb !important}.purple-text.text-accent-2{color:#e040fb !important}.purple.accent-3{background-color:#d500f9 !important}.purple-text.text-accent-3{color:#d500f9 !important}.purple.accent-4{background-color:#a0f !important}.purple-text.text-accent-4{color:#a0f !important}.deep-purple{background-color:#673ab7 !important}.deep-purple-text{color:#673ab7 !important}.deep-purple.lighten-5{background-color:#ede7f6 !important}.deep-purple-text.text-lighten-5{color:#ede7f6 !important}.deep-purple.lighten-4{background-color:#d1c4e9 !important}.deep-purple-text.text-lighten-4{color:#d1c4e9 !important}.deep-purple.lighten-3{background-color:#b39ddb !important}.deep-purple-text.text-lighten-3{color:#b39ddb !important}.deep-purple.lighten-2{background-color:#9575cd !important}.deep-purple-text.text-lighten-2{color:#9575cd !important}.deep-purple.lighten-1{background-color:#7e57c2 !important}.deep-purple-text.text-lighten-1{color:#7e57c2 !important}.deep-purple.darken-1{background-color:#5e35b1 !important}.deep-purple-text.text-darken-1{color:#5e35b1 !important}.deep-purple.darken-2{background-color:#512da8 !important}.deep-purple-text.text-darken-2{color:#512da8 !important}.deep-purple.darken-3{background-color:#4527a0 !important}.deep-purple-text.text-darken-3{color:#4527a0 !important}.deep-purple.darken-4{background-color:#311b92 !important}.deep-purple-text.text-darken-4{color:#311b92 !important}.deep-purple.accent-1{background-color:#b388ff !important}.deep-purple-text.text-accent-1{color:#b388ff !important}.deep-purple.accent-2{background-color:#7c4dff !important}.deep-purple-text.text-accent-2{color:#7c4dff !important}.deep-purple.accent-3{background-color:#651fff !important}.deep-purple-text.text-accent-3{color:#651fff !important}.deep-purple.accent-4{background-color:#6200ea !important}.deep-purple-text.text-accent-4{color:#6200ea !important}.indigo{background-color:#3f51b5 !important}.indigo-text{color:#3f51b5 !important}.indigo.lighten-5{background-color:#e8eaf6 !important}.indigo-text.text-lighten-5{color:#e8eaf6 !important}.indigo.lighten-4{background-color:#c5cae9 !important}.indigo-text.text-lighten-4{color:#c5cae9 !important}.indigo.lighten-3{background-color:#9fa8da !important}.indigo-text.text-lighten-3{color:#9fa8da !important}.indigo.lighten-2{background-color:#7986cb !important}.indigo-text.text-lighten-2{color:#7986cb !important}.indigo.lighten-1{background-color:#5c6bc0 !important}.indigo-text.text-lighten-1{color:#5c6bc0 !important}.indigo.darken-1{background-color:#3949ab !important}.indigo-text.text-darken-1{color:#3949ab !important}.indigo.darken-2{background-color:#303f9f !important}.indigo-text.text-darken-2{color:#303f9f !important}.indigo.darken-3{background-color:#283593 !important}.indigo-text.text-darken-3{color:#283593 !important}.indigo.darken-4{background-color:#1a237e !important}.indigo-text.text-darken-4{color:#1a237e !important}.indigo.accent-1{background-color:#8c9eff !important}.indigo-text.text-accent-1{color:#8c9eff !important}.indigo.accent-2{background-color:#536dfe !important}.indigo-text.text-accent-2{color:#536dfe !important}.indigo.accent-3{background-color:#3d5afe !important}.indigo-text.text-accent-3{color:#3d5afe !important}.indigo.accent-4{background-color:#304ffe !important}.indigo-text.text-accent-4{color:#304ffe !important}.blue{background-color:#2196F3 !important}.blue-text{color:#2196F3 !important}.blue.lighten-5{background-color:#E3F2FD !important}.blue-text.text-lighten-5{color:#E3F2FD !important}.blue.lighten-4{background-color:#BBDEFB !important}.blue-text.text-lighten-4{color:#BBDEFB !important}.blue.lighten-3{background-color:#90CAF9 !important}.blue-text.text-lighten-3{color:#90CAF9 !important}.blue.lighten-2{background-color:#64B5F6 !important}.blue-text.text-lighten-2{color:#64B5F6 !important}.blue.lighten-1{background-color:#42A5F5 !important}.blue-text.text-lighten-1{color:#42A5F5 !important}.blue.darken-1{background-color:#1E88E5 !important}.blue-text.text-darken-1{color:#1E88E5 !important}.blue.darken-2{background-color:#1976D2 !important}.blue-text.text-darken-2{color:#1976D2 !important}.blue.darken-3{background-color:#1565C0 !important}.blue-text.text-darken-3{color:#1565C0 !important}.blue.darken-4{background-color:#0D47A1 !important}.blue-text.text-darken-4{color:#0D47A1 !important}.blue.accent-1{background-color:#82B1FF !important}.blue-text.text-accent-1{color:#82B1FF !important}.blue.accent-2{background-color:#448AFF !important}.blue-text.text-accent-2{color:#448AFF !important}.blue.accent-3{background-color:#2979FF !important}.blue-text.text-accent-3{color:#2979FF !important}.blue.accent-4{background-color:#2962FF !important}.blue-text.text-accent-4{color:#2962FF !important}.light-blue{background-color:#03a9f4 !important}.light-blue-text{color:#03a9f4 !important}.light-blue.lighten-5{background-color:#e1f5fe !important}.light-blue-text.text-lighten-5{color:#e1f5fe !important}.light-blue.lighten-4{background-color:#b3e5fc !important}.light-blue-text.text-lighten-4{color:#b3e5fc !important}.light-blue.lighten-3{background-color:#81d4fa !important}.light-blue-text.text-lighten-3{color:#81d4fa !important}.light-blue.lighten-2{background-color:#4fc3f7 !important}.light-blue-text.text-lighten-2{color:#4fc3f7 !important}.light-blue.lighten-1{background-color:#29b6f6 !important}.light-blue-text.text-lighten-1{color:#29b6f6 !important}.light-blue.darken-1{background-color:#039be5 !important}.light-blue-text.text-darken-1{color:#039be5 !important}.light-blue.darken-2{background-color:#0288d1 !important}.light-blue-text.text-darken-2{color:#0288d1 !important}.light-blue.darken-3{background-color:#0277bd !important}.light-blue-text.text-darken-3{color:#0277bd !important}.light-blue.darken-4{background-color:#01579b !important}.light-blue-text.text-darken-4{color:#01579b !important}.light-blue.accent-1{background-color:#80d8ff !important}.light-blue-text.text-accent-1{color:#80d8ff !important}.light-blue.accent-2{background-color:#40c4ff !important}.light-blue-text.text-accent-2{color:#40c4ff !important}.light-blue.accent-3{background-color:#00b0ff !important}.light-blue-text.text-accent-3{color:#00b0ff !important}.light-blue.accent-4{background-color:#0091ea !important}.light-blue-text.text-accent-4{color:#0091ea !important}.cyan{background-color:#00bcd4 !important}.cyan-text{color:#00bcd4 !important}.cyan.lighten-5{background-color:#e0f7fa !important}.cyan-text.text-lighten-5{color:#e0f7fa !important}.cyan.lighten-4{background-color:#b2ebf2 !important}.cyan-text.text-lighten-4{color:#b2ebf2 !important}.cyan.lighten-3{background-color:#80deea !important}.cyan-text.text-lighten-3{color:#80deea !important}.cyan.lighten-2{background-color:#4dd0e1 !important}.cyan-text.text-lighten-2{color:#4dd0e1 !important}.cyan.lighten-1{background-color:#26c6da !important}.cyan-text.text-lighten-1{color:#26c6da !important}.cyan.darken-1{background-color:#00acc1 !important}.cyan-text.text-darken-1{color:#00acc1 !important}.cyan.darken-2{background-color:#0097a7 !important}.cyan-text.text-darken-2{color:#0097a7 !important}.cyan.darken-3{background-color:#00838f !important}.cyan-text.text-darken-3{color:#00838f !important}.cyan.darken-4{background-color:#006064 !important}.cyan-text.text-darken-4{color:#006064 !important}.cyan.accent-1{background-color:#84ffff !important}.cyan-text.text-accent-1{color:#84ffff !important}.cyan.accent-2{background-color:#18ffff !important}.cyan-text.text-accent-2{color:#18ffff !important}.cyan.accent-3{background-color:#00e5ff !important}.cyan-text.text-accent-3{color:#00e5ff !important}.cyan.accent-4{background-color:#00b8d4 !important}.cyan-text.text-accent-4{color:#00b8d4 !important}.teal{background-color:#009688 !important}.teal-text{color:#009688 !important}.teal.lighten-5{background-color:#e0f2f1 !important}.teal-text.text-lighten-5{color:#e0f2f1 !important}.teal.lighten-4{background-color:#b2dfdb !important}.teal-text.text-lighten-4{color:#b2dfdb !important}.teal.lighten-3{background-color:#80cbc4 !important}.teal-text.text-lighten-3{color:#80cbc4 !important}.teal.lighten-2{background-color:#4db6ac !important}.teal-text.text-lighten-2{color:#4db6ac !important}.teal.lighten-1{background-color:#26a69a !important}.teal-text.text-lighten-1{color:#26a69a !important}.teal.darken-1{background-color:#00897b !important}.teal-text.text-darken-1{color:#00897b !important}.teal.darken-2{background-color:#00796b !important}.teal-text.text-darken-2{color:#00796b !important}.teal.darken-3{background-color:#00695c !important}.teal-text.text-darken-3{color:#00695c !important}.teal.darken-4{background-color:#004d40 !important}.teal-text.text-darken-4{color:#004d40 !important}.teal.accent-1{background-color:#a7ffeb !important}.teal-text.text-accent-1{color:#a7ffeb !important}.teal.accent-2{background-color:#64ffda !important}.teal-text.text-accent-2{color:#64ffda !important}.teal.accent-3{background-color:#1de9b6 !important}.teal-text.text-accent-3{color:#1de9b6 !important}.teal.accent-4{background-color:#00bfa5 !important}.teal-text.text-accent-4{color:#00bfa5 !important}.green{background-color:#4CAF50 !important}.green-text{color:#4CAF50 !important}.green.lighten-5{background-color:#E8F5E9 !important}.green-text.text-lighten-5{color:#E8F5E9 !important}.green.lighten-4{background-color:#C8E6C9 !important}.green-text.text-lighten-4{color:#C8E6C9 !important}.green.lighten-3{background-color:#A5D6A7 !important}.green-text.text-lighten-3{color:#A5D6A7 !important}.green.lighten-2{background-color:#81C784 !important}.green-text.text-lighten-2{color:#81C784 !important}.green.lighten-1{background-color:#66BB6A !important}.green-text.text-lighten-1{color:#66BB6A !important}.green.darken-1{background-color:#43A047 !important}.green-text.text-darken-1{color:#43A047 !important}.green.darken-2{background-color:#388E3C !important}.green-text.text-darken-2{color:#388E3C !important}.green.darken-3{background-color:#2E7D32 !important}.green-text.text-darken-3{color:#2E7D32 !important}.green.darken-4{background-color:#1B5E20 !important}.green-text.text-darken-4{color:#1B5E20 !important}.green.accent-1{background-color:#B9F6CA !important}.green-text.text-accent-1{color:#B9F6CA !important}.green.accent-2{background-color:#69F0AE !important}.green-text.text-accent-2{color:#69F0AE !important}.green.accent-3{background-color:#00E676 !important}.green-text.text-accent-3{color:#00E676 !important}.green.accent-4{background-color:#00C853 !important}.green-text.text-accent-4{color:#00C853 !important}.light-green{background-color:#8bc34a !important}.light-green-text{color:#8bc34a !important}.light-green.lighten-5{background-color:#f1f8e9 !important}.light-green-text.text-lighten-5{color:#f1f8e9 !important}.light-green.lighten-4{background-color:#dcedc8 !important}.light-green-text.text-lighten-4{color:#dcedc8 !important}.light-green.lighten-3{background-color:#c5e1a5 !important}.light-green-text.text-lighten-3{color:#c5e1a5 !important}.light-green.lighten-2{background-color:#aed581 !important}.light-green-text.text-lighten-2{color:#aed581 !important}.light-green.lighten-1{background-color:#9ccc65 !important}.light-green-text.text-lighten-1{color:#9ccc65 !important}.light-green.darken-1{background-color:#7cb342 !important}.light-green-text.text-darken-1{color:#7cb342 !important}.light-green.darken-2{background-color:#689f38 !important}.light-green-text.text-darken-2{color:#689f38 !important}.light-green.darken-3{background-color:#558b2f !important}.light-green-text.text-darken-3{color:#558b2f !important}.light-green.darken-4{background-color:#33691e !important}.light-green-text.text-darken-4{color:#33691e !important}.light-green.accent-1{background-color:#ccff90 !important}.light-green-text.text-accent-1{color:#ccff90 !important}.light-green.accent-2{background-color:#b2ff59 !important}.light-green-text.text-accent-2{color:#b2ff59 !important}.light-green.accent-3{background-color:#76ff03 !important}.light-green-text.text-accent-3{color:#76ff03 !important}.light-green.accent-4{background-color:#64dd17 !important}.light-green-text.text-accent-4{color:#64dd17 !important}.lime{background-color:#cddc39 !important}.lime-text{color:#cddc39 !important}.lime.lighten-5{background-color:#f9fbe7 !important}.lime-text.text-lighten-5{color:#f9fbe7 !important}.lime.lighten-4{background-color:#f0f4c3 !important}.lime-text.text-lighten-4{color:#f0f4c3 !important}.lime.lighten-3{background-color:#e6ee9c !important}.lime-text.text-lighten-3{color:#e6ee9c !important}.lime.lighten-2{background-color:#dce775 !important}.lime-text.text-lighten-2{color:#dce775 !important}.lime.lighten-1{background-color:#d4e157 !important}.lime-text.text-lighten-1{color:#d4e157 !important}.lime.darken-1{background-color:#c0ca33 !important}.lime-text.text-darken-1{color:#c0ca33 !important}.lime.darken-2{background-color:#afb42b !important}.lime-text.text-darken-2{color:#afb42b !important}.lime.darken-3{background-color:#9e9d24 !important}.lime-text.text-darken-3{color:#9e9d24 !important}.lime.darken-4{background-color:#827717 !important}.lime-text.text-darken-4{color:#827717 !important}.lime.accent-1{background-color:#f4ff81 !important}.lime-text.text-accent-1{color:#f4ff81 !important}.lime.accent-2{background-color:#eeff41 !important}.lime-text.text-accent-2{color:#eeff41 !important}.lime.accent-3{background-color:#c6ff00 !important}.lime-text.text-accent-3{color:#c6ff00 !important}.lime.accent-4{background-color:#aeea00 !important}.lime-text.text-accent-4{color:#aeea00 !important}.yellow{background-color:#ffeb3b !important}.yellow-text{color:#ffeb3b !important}.yellow.lighten-5{background-color:#fffde7 !important}.yellow-text.text-lighten-5{color:#fffde7 !important}.yellow.lighten-4{background-color:#fff9c4 !important}.yellow-text.text-lighten-4{color:#fff9c4 !important}.yellow.lighten-3{background-color:#fff59d !important}.yellow-text.text-lighten-3{color:#fff59d !important}.yellow.lighten-2{background-color:#fff176 !important}.yellow-text.text-lighten-2{color:#fff176 !important}.yellow.lighten-1{background-color:#ffee58 !important}.yellow-text.text-lighten-1{color:#ffee58 !important}.yellow.darken-1{background-color:#fdd835 !important}.yellow-text.text-darken-1{color:#fdd835 !important}.yellow.darken-2{background-color:#fbc02d !important}.yellow-text.text-darken-2{color:#fbc02d !important}.yellow.darken-3{background-color:#f9a825 !important}.yellow-text.text-darken-3{color:#f9a825 !important}.yellow.darken-4{background-color:#f57f17 !important}.yellow-text.text-darken-4{color:#f57f17 !important}.yellow.accent-1{background-color:#ffff8d !important}.yellow-text.text-accent-1{color:#ffff8d !important}.yellow.accent-2{background-color:#ff0 !important}.yellow-text.text-accent-2{color:#ff0 !important}.yellow.accent-3{background-color:#ffea00 !important}.yellow-text.text-accent-3{color:#ffea00 !important}.yellow.accent-4{background-color:#ffd600 !important}.yellow-text.text-accent-4{color:#ffd600 !important}.amber{background-color:#ffc107 !important}.amber-text{color:#ffc107 !important}.amber.lighten-5{background-color:#fff8e1 !important}.amber-text.text-lighten-5{color:#fff8e1 !important}.amber.lighten-4{background-color:#ffecb3 !important}.amber-text.text-lighten-4{color:#ffecb3 !important}.amber.lighten-3{background-color:#ffe082 !important}.amber-text.text-lighten-3{color:#ffe082 !important}.amber.lighten-2{background-color:#ffd54f !important}.amber-text.text-lighten-2{color:#ffd54f !important}.amber.lighten-1{background-color:#ffca28 !important}.amber-text.text-lighten-1{color:#ffca28 !important}.amber.darken-1{background-color:#ffb300 !important}.amber-text.text-darken-1{color:#ffb300 !important}.amber.darken-2{background-color:#ffa000 !important}.amber-text.text-darken-2{color:#ffa000 !important}.amber.darken-3{background-color:#ff8f00 !important}.amber-text.text-darken-3{color:#ff8f00 !important}.amber.darken-4{background-color:#ff6f00 !important}.amber-text.text-darken-4{color:#ff6f00 !important}.amber.accent-1{background-color:#ffe57f !important}.amber-text.text-accent-1{color:#ffe57f !important}.amber.accent-2{background-color:#ffd740 !important}.amber-text.text-accent-2{color:#ffd740 !important}.amber.accent-3{background-color:#ffc400 !important}.amber-text.text-accent-3{color:#ffc400 !important}.amber.accent-4{background-color:#ffab00 !important}.amber-text.text-accent-4{color:#ffab00 !important}.orange{background-color:#ff9800 !important}.orange-text{color:#ff9800 !important}.orange.lighten-5{background-color:#fff3e0 !important}.orange-text.text-lighten-5{color:#fff3e0 !important}.orange.lighten-4{background-color:#ffe0b2 !important}.orange-text.text-lighten-4{color:#ffe0b2 !important}.orange.lighten-3{background-color:#ffcc80 !important}.orange-text.text-lighten-3{color:#ffcc80 !important}.orange.lighten-2{background-color:#ffb74d !important}.orange-text.text-lighten-2{color:#ffb74d !important}.orange.lighten-1{background-color:#ffa726 !important}.orange-text.text-lighten-1{color:#ffa726 !important}.orange.darken-1{background-color:#fb8c00 !important}.orange-text.text-darken-1{color:#fb8c00 !important}.orange.darken-2{background-color:#f57c00 !important}.orange-text.text-darken-2{color:#f57c00 !important}.orange.darken-3{background-color:#ef6c00 !important}.orange-text.text-darken-3{color:#ef6c00 !important}.orange.darken-4{background-color:#e65100 !important}.orange-text.text-darken-4{color:#e65100 !important}.orange.accent-1{background-color:#ffd180 !important}.orange-text.text-accent-1{color:#ffd180 !important}.orange.accent-2{background-color:#ffab40 !important}.orange-text.text-accent-2{color:#ffab40 !important}.orange.accent-3{background-color:#ff9100 !important}.orange-text.text-accent-3{color:#ff9100 !important}.orange.accent-4{background-color:#ff6d00 !important}.orange-text.text-accent-4{color:#ff6d00 !important}.deep-orange{background-color:#ff5722 !important}.deep-orange-text{color:#ff5722 !important}.deep-orange.lighten-5{background-color:#fbe9e7 !important}.deep-orange-text.text-lighten-5{color:#fbe9e7 !important}.deep-orange.lighten-4{background-color:#ffccbc !important}.deep-orange-text.text-lighten-4{color:#ffccbc !important}.deep-orange.lighten-3{background-color:#ffab91 !important}.deep-orange-text.text-lighten-3{color:#ffab91 !important}.deep-orange.lighten-2{background-color:#ff8a65 !important}.deep-orange-text.text-lighten-2{color:#ff8a65 !important}.deep-orange.lighten-1{background-color:#ff7043 !important}.deep-orange-text.text-lighten-1{color:#ff7043 !important}.deep-orange.darken-1{background-color:#f4511e !important}.deep-orange-text.text-darken-1{color:#f4511e !important}.deep-orange.darken-2{background-color:#e64a19 !important}.deep-orange-text.text-darken-2{color:#e64a19 !important}.deep-orange.darken-3{background-color:#d84315 !important}.deep-orange-text.text-darken-3{color:#d84315 !important}.deep-orange.darken-4{background-color:#bf360c !important}.deep-orange-text.text-darken-4{color:#bf360c !important}.deep-orange.accent-1{background-color:#ff9e80 !important}.deep-orange-text.text-accent-1{color:#ff9e80 !important}.deep-orange.accent-2{background-color:#ff6e40 !important}.deep-orange-text.text-accent-2{color:#ff6e40 !important}.deep-orange.accent-3{background-color:#ff3d00 !important}.deep-orange-text.text-accent-3{color:#ff3d00 !important}.deep-orange.accent-4{background-color:#dd2c00 !important}.deep-orange-text.text-accent-4{color:#dd2c00 !important}.brown{background-color:#795548 !important}.brown-text{color:#795548 !important}.brown.lighten-5{background-color:#efebe9 !important}.brown-text.text-lighten-5{color:#efebe9 !important}.brown.lighten-4{background-color:#d7ccc8 !important}.brown-text.text-lighten-4{color:#d7ccc8 !important}.brown.lighten-3{background-color:#bcaaa4 !important}.brown-text.text-lighten-3{color:#bcaaa4 !important}.brown.lighten-2{background-color:#a1887f !important}.brown-text.text-lighten-2{color:#a1887f !important}.brown.lighten-1{background-color:#8d6e63 !important}.brown-text.text-lighten-1{color:#8d6e63 !important}.brown.darken-1{background-color:#6d4c41 !important}.brown-text.text-darken-1{color:#6d4c41 !important}.brown.darken-2{background-color:#5d4037 !important}.brown-text.text-darken-2{color:#5d4037 !important}.brown.darken-3{background-color:#4e342e !important}.brown-text.text-darken-3{color:#4e342e !important}.brown.darken-4{background-color:#3e2723 !important}.brown-text.text-darken-4{color:#3e2723 !important}.blue-grey{background-color:#607d8b !important}.blue-grey-text{color:#607d8b !important}.blue-grey.lighten-5{background-color:#eceff1 !important}.blue-grey-text.text-lighten-5{color:#eceff1 !important}.blue-grey.lighten-4{background-color:#cfd8dc !important}.blue-grey-text.text-lighten-4{color:#cfd8dc !important}.blue-grey.lighten-3{background-color:#b0bec5 !important}.blue-grey-text.text-lighten-3{color:#b0bec5 !important}.blue-grey.lighten-2{background-color:#90a4ae !important}.blue-grey-text.text-lighten-2{color:#90a4ae !important}.blue-grey.lighten-1{background-color:#78909c !important}.blue-grey-text.text-lighten-1{color:#78909c !important}.blue-grey.darken-1{background-color:#546e7a !important}.blue-grey-text.text-darken-1{color:#546e7a !important}.blue-grey.darken-2{background-color:#455a64 !important}.blue-grey-text.text-darken-2{color:#455a64 !important}.blue-grey.darken-3{background-color:#37474f !important}.blue-grey-text.text-darken-3{color:#37474f !important}.blue-grey.darken-4{background-color:#263238 !important}.blue-grey-text.text-darken-4{color:#263238 !important}.grey{background-color:#9e9e9e !important}.grey-text{color:#9e9e9e !important}.grey.lighten-5{background-color:#fafafa !important}.grey-text.text-lighten-5{color:#fafafa !important}.grey.lighten-4{background-color:#f5f5f5 !important}.grey-text.text-lighten-4{color:#f5f5f5 !important}.grey.lighten-3{background-color:#eee !important}.grey-text.text-lighten-3{color:#eee !important}.grey.lighten-2{background-color:#e0e0e0 !important}.grey-text.text-lighten-2{color:#e0e0e0 !important}.grey.lighten-1{background-color:#bdbdbd !important}.grey-text.text-lighten-1{color:#bdbdbd !important}.grey.darken-1{background-color:#757575 !important}.grey-text.text-darken-1{color:#757575 !important}.grey.darken-2{background-color:#616161 !important}.grey-text.text-darken-2{color:#616161 !important}.grey.darken-3{background-color:#424242 !important}.grey-text.text-darken-3{color:#424242 !important}.grey.darken-4{background-color:#212121 !important}.grey-text.text-darken-4{color:#212121 !important}.black{background-color:#000 !important}.black-text{color:#000 !important}.white{background-color:#fff !important}.white-text{color:#fff !important}.transparent{background-color:rgba(0,0,0,0) !important}.transparent-text{color:rgba(0,0,0,0) !important}/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:0.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace, monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}html{-webkit-box-sizing:border-box;box-sizing:border-box}*,*:before,*:after{-webkit-box-sizing:inherit;box-sizing:inherit}button,input,optgroup,select,textarea{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}ul:not(.browser-default){padding-left:0;list-style-type:none}ul:not(.browser-default)>li{list-style-type:none}a{color:#039be5;text-decoration:none;-webkit-tap-highlight-color:transparent}.valign-wrapper{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.clearfix{clear:both}.z-depth-0{-webkit-box-shadow:none !important;box-shadow:none !important}.z-depth-1,nav,.card-panel,.card,.toast,.btn,.btn-large,.btn-small,.btn-floating,.dropdown-content,.collapsible,.sidenav{-webkit-box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2);box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2)}.z-depth-1-half,.btn:hover,.btn-large:hover,.btn-small:hover,.btn-floating:hover{-webkit-box-shadow:0 3px 3px 0 rgba(0,0,0,0.14),0 1px 7px 0 rgba(0,0,0,0.12),0 3px 1px -1px rgba(0,0,0,0.2);box-shadow:0 3px 3px 0 rgba(0,0,0,0.14),0 1px 7px 0 rgba(0,0,0,0.12),0 3px 1px -1px rgba(0,0,0,0.2)}.z-depth-2{-webkit-box-shadow:0 4px 5px 0 rgba(0,0,0,0.14),0 1px 10px 0 rgba(0,0,0,0.12),0 2px 4px -1px rgba(0,0,0,0.3);box-shadow:0 4px 5px 0 rgba(0,0,0,0.14),0 1px 10px 0 rgba(0,0,0,0.12),0 2px 4px -1px rgba(0,0,0,0.3)}.z-depth-3{-webkit-box-shadow:0 8px 17px 2px rgba(0,0,0,0.14),0 3px 14px 2px rgba(0,0,0,0.12),0 5px 5px -3px rgba(0,0,0,0.2);box-shadow:0 8px 17px 2px rgba(0,0,0,0.14),0 3px 14px 2px rgba(0,0,0,0.12),0 5px 5px -3px rgba(0,0,0,0.2)}.z-depth-4{-webkit-box-shadow:0 16px 24px 2px rgba(0,0,0,0.14),0 6px 30px 5px rgba(0,0,0,0.12),0 8px 10px -7px rgba(0,0,0,0.2);box-shadow:0 16px 24px 2px rgba(0,0,0,0.14),0 6px 30px 5px rgba(0,0,0,0.12),0 8px 10px -7px rgba(0,0,0,0.2)}.z-depth-5,.modal{-webkit-box-shadow:0 24px 38px 3px rgba(0,0,0,0.14),0 9px 46px 8px rgba(0,0,0,0.12),0 11px 15px -7px rgba(0,0,0,0.2);box-shadow:0 24px 38px 3px rgba(0,0,0,0.14),0 9px 46px 8px rgba(0,0,0,0.12),0 11px 15px -7px rgba(0,0,0,0.2)}.hoverable{-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s}.hoverable:hover{-webkit-box-shadow:0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);box-shadow:0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}.divider{height:1px;overflow:hidden;background-color:#e0e0e0}blockquote{margin:20px 0;padding-left:1.5rem;border-left:5px solid #ee6e73}i{line-height:inherit}i.left{float:left;margin-right:15px}i.right{float:right;margin-left:15px}i.tiny{font-size:1rem}i.small{font-size:2rem}i.medium{font-size:4rem}i.large{font-size:6rem}img.responsive-img,video.responsive-video{max-width:100%;height:auto}.pagination li{display:inline-block;border-radius:2px;text-align:center;vertical-align:top;height:30px}.pagination li a{color:#444;display:inline-block;font-size:1.2rem;padding:0 10px;line-height:30px}.pagination li.active a{color:#fff}.pagination li.active{background-color:#ee6e73}.pagination li.disabled a{cursor:default;color:#999}.pagination li i{font-size:2rem}.pagination li.pages ul li{display:inline-block;float:none}@media only screen and (max-width: 992px){.pagination{width:100%}.pagination li.prev,.pagination li.next{width:10%}.pagination li.pages{width:80%;overflow:hidden;white-space:nowrap}}.breadcrumb{font-size:18px;color:rgba(255,255,255,0.7)}.breadcrumb i,.breadcrumb [class^="mdi-"],.breadcrumb [class*="mdi-"],.breadcrumb i.material-icons{display:inline-block;float:left;font-size:24px}.breadcrumb:before{content:'\E5CC';color:rgba(255,255,255,0.7);vertical-align:top;display:inline-block;font-family:'Material Icons';font-weight:normal;font-style:normal;font-size:25px;margin:0 10px 0 8px;-webkit-font-smoothing:antialiased}.breadcrumb:first-child:before{display:none}.breadcrumb:last-child{color:#fff}.parallax-container{position:relative;overflow:hidden;height:500px}.parallax-container .parallax{position:absolute;top:0;left:0;right:0;bottom:0;z-index:-1}.parallax-container .parallax img{opacity:0;position:absolute;left:50%;bottom:0;min-width:100%;min-height:100%;-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);-webkit-transform:translateX(-50%);transform:translateX(-50%)}.pin-top,.pin-bottom{position:relative}.pinned{position:fixed !important}ul.staggered-list li{opacity:0}.fade-in{opacity:0;-webkit-transform-origin:0 50%;transform-origin:0 50%}@media only screen and (max-width: 600px){.hide-on-small-only,.hide-on-small-and-down{display:none !important}}@media only screen and (max-width: 992px){.hide-on-med-and-down{display:none !important}}@media only screen and (min-width: 601px){.hide-on-med-and-up{display:none !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.hide-on-med-only{display:none !important}}@media only screen and (min-width: 993px){.hide-on-large-only{display:none !important}}@media only screen and (min-width: 1201px){.hide-on-extra-large-only{display:none !important}}@media only screen and (min-width: 1201px){.show-on-extra-large{display:block !important}}@media only screen and (min-width: 993px){.show-on-large{display:block !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.show-on-medium{display:block !important}}@media only screen and (max-width: 600px){.show-on-small{display:block !important}}@media only screen and (min-width: 601px){.show-on-medium-and-up{display:block !important}}@media only screen and (max-width: 992px){.show-on-medium-and-down{display:block !important}}@media only screen and (max-width: 600px){.center-on-small-only{text-align:center}}.page-footer{padding-top:20px;color:#fff;background-color:#ee6e73}.page-footer .footer-copyright{overflow:hidden;min-height:50px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:10px 0px;color:rgba(255,255,255,0.8);background-color:rgba(51,51,51,0.08)}table,th,td{border:none}table{width:100%;display:table;border-collapse:collapse;border-spacing:0}table.striped tr{border-bottom:none}table.striped>tbody>tr:nth-child(odd){background-color:rgba(242,242,242,0.5)}table.striped>tbody>tr>td{border-radius:0}table.highlight>tbody>tr{-webkit-transition:background-color .25s ease;transition:background-color .25s ease}table.highlight>tbody>tr:hover{background-color:rgba(242,242,242,0.5)}table.centered thead tr th,table.centered tbody tr td{text-align:center}tr{border-bottom:1px solid rgba(0,0,0,0.12)}td,th{padding:15px 5px;display:table-cell;text-align:left;vertical-align:middle;border-radius:2px}@media only screen and (max-width: 992px){table.responsive-table{width:100%;border-collapse:collapse;border-spacing:0;display:block;position:relative}table.responsive-table td:empty:before{content:'\00a0'}table.responsive-table th,table.responsive-table td{margin:0;vertical-align:top}table.responsive-table th{text-align:left}table.responsive-table thead{display:block;float:left}table.responsive-table thead tr{display:block;padding:0 10px 0 0}table.responsive-table thead tr th::before{content:"\00a0"}table.responsive-table tbody{display:block;width:auto;position:relative;overflow-x:auto;white-space:nowrap}table.responsive-table tbody tr{display:inline-block;vertical-align:top}table.responsive-table th{display:block;text-align:right}table.responsive-table td{display:block;min-height:1.25em;text-align:left}table.responsive-table tr{border-bottom:none;padding:0 10px}table.responsive-table thead{border:0;border-right:1px solid rgba(0,0,0,0.12)}}.collection{margin:.5rem 0 1rem 0;border:1px solid #e0e0e0;border-radius:2px;overflow:hidden;position:relative}.collection .collection-item{background-color:#fff;line-height:1.5rem;padding:10px 20px;margin:0;border-bottom:1px solid #e0e0e0}.collection .collection-item.avatar{min-height:84px;padding-left:72px;position:relative}.collection .collection-item.avatar:not(.circle-clipper)>.circle,.collection .collection-item.avatar :not(.circle-clipper)>.circle{position:absolute;width:42px;height:42px;overflow:hidden;left:15px;display:inline-block;vertical-align:middle}.collection .collection-item.avatar i.circle{font-size:18px;line-height:42px;color:#fff;background-color:#999;text-align:center}.collection .collection-item.avatar .title{font-size:16px}.collection .collection-item.avatar p{margin:0}.collection .collection-item.avatar .secondary-content{position:absolute;top:16px;right:16px}.collection .collection-item:last-child{border-bottom:none}.collection .collection-item.active{background-color:#26a69a;color:#eafaf9}.collection .collection-item.active .secondary-content{color:#fff}.collection a.collection-item{display:block;-webkit-transition:.25s;transition:.25s;color:#26a69a}.collection a.collection-item:not(.active):hover{background-color:#ddd}.collection.with-header .collection-header{background-color:#fff;border-bottom:1px solid #e0e0e0;padding:10px 20px}.collection.with-header .collection-item{padding-left:30px}.collection.with-header .collection-item.avatar{padding-left:72px}.secondary-content{float:right;color:#26a69a}.collapsible .collection{margin:0;border:none}.video-container{position:relative;padding-bottom:56.25%;height:0;overflow:hidden}.video-container iframe,.video-container object,.video-container embed{position:absolute;top:0;left:0;width:100%;height:100%}.progress{position:relative;height:4px;display:block;width:100%;background-color:#acece6;border-radius:2px;margin:.5rem 0 1rem 0;overflow:hidden}.progress .determinate{position:absolute;top:0;left:0;bottom:0;background-color:#26a69a;-webkit-transition:width .3s linear;transition:width .3s linear}.progress .indeterminate{background-color:#26a69a}.progress .indeterminate:before{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;-webkit-animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite}.progress .indeterminate:after{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;-webkit-animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;-webkit-animation-delay:1.15s;animation-delay:1.15s}@-webkit-keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@-webkit-keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}@keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}.hide{display:none !important}.left-align{text-align:left}.right-align{text-align:right}.center,.center-align{text-align:center}.left{float:left !important}.right{float:right !important}.no-select,input[type=range],input[type=range]+.thumb{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.circle{border-radius:50%}.center-block{display:block;margin-left:auto;margin-right:auto}.truncate{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-padding{padding:0 !important}span.badge{min-width:3rem;padding:0 6px;margin-left:14px;text-align:center;font-size:1rem;line-height:22px;height:22px;color:#757575;float:right;-webkit-box-sizing:border-box;box-sizing:border-box}span.badge.new{font-weight:300;font-size:0.8rem;color:#fff;background-color:#26a69a;border-radius:2px}span.badge.new:after{content:" new"}span.badge[data-badge-caption]::after{content:" " attr(data-badge-caption)}nav ul a span.badge{display:inline-block;float:none;margin-left:4px;line-height:22px;height:22px;-webkit-font-smoothing:auto}.collection-item span.badge{margin-top:calc(.75rem - 11px)}.collapsible span.badge{margin-left:auto}.sidenav span.badge{margin-top:calc(24px - 11px)}table span.badge{display:inline-block;float:none;margin-left:auto}.material-icons{text-rendering:optimizeLegibility;-webkit-font-feature-settings:'liga';-moz-font-feature-settings:'liga';font-feature-settings:'liga'}.container{margin:0 auto;max-width:1280px;width:90%}@media only screen and (min-width: 601px){.container{width:85%}}@media only screen and (min-width: 993px){.container{width:70%}}.col .row{margin-left:-.75rem;margin-right:-.75rem}.section{padding-top:1rem;padding-bottom:1rem}.section.no-pad{padding:0}.section.no-pad-bot{padding-bottom:0}.section.no-pad-top{padding-top:0}.row{margin-left:auto;margin-right:auto;margin-bottom:20px}.row:after{content:"";display:table;clear:both}.row .col{float:left;-webkit-box-sizing:border-box;box-sizing:border-box;padding:0 .75rem;min-height:1px}.row .col[class*="push-"],.row .col[class*="pull-"]{position:relative}.row .col.s1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.s4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.s7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.s10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-s1{margin-left:8.3333333333%}.row .col.pull-s1{right:8.3333333333%}.row .col.push-s1{left:8.3333333333%}.row .col.offset-s2{margin-left:16.6666666667%}.row .col.pull-s2{right:16.6666666667%}.row .col.push-s2{left:16.6666666667%}.row .col.offset-s3{margin-left:25%}.row .col.pull-s3{right:25%}.row .col.push-s3{left:25%}.row .col.offset-s4{margin-left:33.3333333333%}.row .col.pull-s4{right:33.3333333333%}.row .col.push-s4{left:33.3333333333%}.row .col.offset-s5{margin-left:41.6666666667%}.row .col.pull-s5{right:41.6666666667%}.row .col.push-s5{left:41.6666666667%}.row .col.offset-s6{margin-left:50%}.row .col.pull-s6{right:50%}.row .col.push-s6{left:50%}.row .col.offset-s7{margin-left:58.3333333333%}.row .col.pull-s7{right:58.3333333333%}.row .col.push-s7{left:58.3333333333%}.row .col.offset-s8{margin-left:66.6666666667%}.row .col.pull-s8{right:66.6666666667%}.row .col.push-s8{left:66.6666666667%}.row .col.offset-s9{margin-left:75%}.row .col.pull-s9{right:75%}.row .col.push-s9{left:75%}.row .col.offset-s10{margin-left:83.3333333333%}.row .col.pull-s10{right:83.3333333333%}.row .col.push-s10{left:83.3333333333%}.row .col.offset-s11{margin-left:91.6666666667%}.row .col.pull-s11{right:91.6666666667%}.row .col.push-s11{left:91.6666666667%}.row .col.offset-s12{margin-left:100%}.row .col.pull-s12{right:100%}.row .col.push-s12{left:100%}@media only screen and (min-width: 601px){.row .col.m1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.m4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.m7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.m10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-m1{margin-left:8.3333333333%}.row .col.pull-m1{right:8.3333333333%}.row .col.push-m1{left:8.3333333333%}.row .col.offset-m2{margin-left:16.6666666667%}.row .col.pull-m2{right:16.6666666667%}.row .col.push-m2{left:16.6666666667%}.row .col.offset-m3{margin-left:25%}.row .col.pull-m3{right:25%}.row .col.push-m3{left:25%}.row .col.offset-m4{margin-left:33.3333333333%}.row .col.pull-m4{right:33.3333333333%}.row .col.push-m4{left:33.3333333333%}.row .col.offset-m5{margin-left:41.6666666667%}.row .col.pull-m5{right:41.6666666667%}.row .col.push-m5{left:41.6666666667%}.row .col.offset-m6{margin-left:50%}.row .col.pull-m6{right:50%}.row .col.push-m6{left:50%}.row .col.offset-m7{margin-left:58.3333333333%}.row .col.pull-m7{right:58.3333333333%}.row .col.push-m7{left:58.3333333333%}.row .col.offset-m8{margin-left:66.6666666667%}.row .col.pull-m8{right:66.6666666667%}.row .col.push-m8{left:66.6666666667%}.row .col.offset-m9{margin-left:75%}.row .col.pull-m9{right:75%}.row .col.push-m9{left:75%}.row .col.offset-m10{margin-left:83.3333333333%}.row .col.pull-m10{right:83.3333333333%}.row .col.push-m10{left:83.3333333333%}.row .col.offset-m11{margin-left:91.6666666667%}.row .col.pull-m11{right:91.6666666667%}.row .col.push-m11{left:91.6666666667%}.row .col.offset-m12{margin-left:100%}.row .col.pull-m12{right:100%}.row .col.push-m12{left:100%}}@media only screen and (min-width: 993px){.row .col.l1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.l4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.l7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.l10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-l1{margin-left:8.3333333333%}.row .col.pull-l1{right:8.3333333333%}.row .col.push-l1{left:8.3333333333%}.row .col.offset-l2{margin-left:16.6666666667%}.row .col.pull-l2{right:16.6666666667%}.row .col.push-l2{left:16.6666666667%}.row .col.offset-l3{margin-left:25%}.row .col.pull-l3{right:25%}.row .col.push-l3{left:25%}.row .col.offset-l4{margin-left:33.3333333333%}.row .col.pull-l4{right:33.3333333333%}.row .col.push-l4{left:33.3333333333%}.row .col.offset-l5{margin-left:41.6666666667%}.row .col.pull-l5{right:41.6666666667%}.row .col.push-l5{left:41.6666666667%}.row .col.offset-l6{margin-left:50%}.row .col.pull-l6{right:50%}.row .col.push-l6{left:50%}.row .col.offset-l7{margin-left:58.3333333333%}.row .col.pull-l7{right:58.3333333333%}.row .col.push-l7{left:58.3333333333%}.row .col.offset-l8{margin-left:66.6666666667%}.row .col.pull-l8{right:66.6666666667%}.row .col.push-l8{left:66.6666666667%}.row .col.offset-l9{margin-left:75%}.row .col.pull-l9{right:75%}.row .col.push-l9{left:75%}.row .col.offset-l10{margin-left:83.3333333333%}.row .col.pull-l10{right:83.3333333333%}.row .col.push-l10{left:83.3333333333%}.row .col.offset-l11{margin-left:91.6666666667%}.row .col.pull-l11{right:91.6666666667%}.row .col.push-l11{left:91.6666666667%}.row .col.offset-l12{margin-left:100%}.row .col.pull-l12{right:100%}.row .col.push-l12{left:100%}}@media only screen and (min-width: 1201px){.row .col.xl1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.xl4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.xl7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.xl10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-xl1{margin-left:8.3333333333%}.row .col.pull-xl1{right:8.3333333333%}.row .col.push-xl1{left:8.3333333333%}.row .col.offset-xl2{margin-left:16.6666666667%}.row .col.pull-xl2{right:16.6666666667%}.row .col.push-xl2{left:16.6666666667%}.row .col.offset-xl3{margin-left:25%}.row .col.pull-xl3{right:25%}.row .col.push-xl3{left:25%}.row .col.offset-xl4{margin-left:33.3333333333%}.row .col.pull-xl4{right:33.3333333333%}.row .col.push-xl4{left:33.3333333333%}.row .col.offset-xl5{margin-left:41.6666666667%}.row .col.pull-xl5{right:41.6666666667%}.row .col.push-xl5{left:41.6666666667%}.row .col.offset-xl6{margin-left:50%}.row .col.pull-xl6{right:50%}.row .col.push-xl6{left:50%}.row .col.offset-xl7{margin-left:58.3333333333%}.row .col.pull-xl7{right:58.3333333333%}.row .col.push-xl7{left:58.3333333333%}.row .col.offset-xl8{margin-left:66.6666666667%}.row .col.pull-xl8{right:66.6666666667%}.row .col.push-xl8{left:66.6666666667%}.row .col.offset-xl9{margin-left:75%}.row .col.pull-xl9{right:75%}.row .col.push-xl9{left:75%}.row .col.offset-xl10{margin-left:83.3333333333%}.row .col.pull-xl10{right:83.3333333333%}.row .col.push-xl10{left:83.3333333333%}.row .col.offset-xl11{margin-left:91.6666666667%}.row .col.pull-xl11{right:91.6666666667%}.row .col.push-xl11{left:91.6666666667%}.row .col.offset-xl12{margin-left:100%}.row .col.pull-xl12{right:100%}.row .col.push-xl12{left:100%}}nav{color:#fff;background-color:#ee6e73;width:100%;height:56px;line-height:56px}nav.nav-extended{height:auto}nav.nav-extended .nav-wrapper{min-height:56px;height:auto}nav.nav-extended .nav-content{position:relative;line-height:normal}nav a{color:#fff}nav i,nav [class^="mdi-"],nav [class*="mdi-"],nav i.material-icons{display:block;font-size:24px;height:56px;line-height:56px}nav .nav-wrapper{position:relative;height:100%}@media only screen and (min-width: 993px){nav a.sidenav-trigger{display:none}}nav .sidenav-trigger{float:left;position:relative;z-index:1;height:56px;margin:0 18px}nav .sidenav-trigger i{height:56px;line-height:56px}nav .brand-logo{position:absolute;color:#fff;display:inline-block;font-size:2.1rem;padding:0}nav .brand-logo.center{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}@media only screen and (max-width: 992px){nav .brand-logo{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}nav .brand-logo.left,nav .brand-logo.right{padding:0;-webkit-transform:none;transform:none}nav .brand-logo.left{left:0.5rem}nav .brand-logo.right{right:0.5rem;left:auto}}nav .brand-logo.right{right:0.5rem;padding:0}nav .brand-logo i,nav .brand-logo [class^="mdi-"],nav .brand-logo [class*="mdi-"],nav .brand-logo i.material-icons{float:left;margin-right:15px}nav .nav-title{display:inline-block;font-size:32px;padding:28px 0}nav ul{margin:0}nav ul li{-webkit-transition:background-color .3s;transition:background-color .3s;float:left;padding:0}nav ul li.active{background-color:rgba(0,0,0,0.1)}nav ul a{-webkit-transition:background-color .3s;transition:background-color .3s;font-size:1rem;color:#fff;display:block;padding:0 15px;cursor:pointer}nav ul a.btn,nav ul a.btn-large,nav ul a.btn-small,nav ul a.btn-large,nav ul a.btn-flat,nav ul a.btn-floating{margin-top:-2px;margin-left:15px;margin-right:15px}nav ul a.btn>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn-small>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn-flat>.material-icons,nav ul a.btn-floating>.material-icons{height:inherit;line-height:inherit}nav ul a:hover{background-color:rgba(0,0,0,0.1)}nav ul.left{float:left}nav form{height:100%}nav .input-field{margin:0;height:100%}nav .input-field input{height:100%;font-size:1.2rem;border:none;padding-left:2rem}nav .input-field input:focus,nav .input-field input[type=text]:valid,nav .input-field input[type=password]:valid,nav .input-field input[type=email]:valid,nav .input-field input[type=url]:valid,nav .input-field input[type=date]:valid{border:none;-webkit-box-shadow:none;box-shadow:none}nav .input-field label{top:0;left:0}nav .input-field label i{color:rgba(255,255,255,0.7);-webkit-transition:color .3s;transition:color .3s}nav .input-field label.active i{color:#fff}.navbar-fixed{position:relative;height:56px;z-index:997}.navbar-fixed nav{position:fixed}@media only screen and (min-width: 601px){nav.nav-extended .nav-wrapper{min-height:64px}nav,nav .nav-wrapper i,nav a.sidenav-trigger,nav a.sidenav-trigger i{height:64px;line-height:64px}.navbar-fixed{height:64px}}a{text-decoration:none}html{line-height:1.5;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-weight:normal;color:rgba(0,0,0,0.87)}@media only screen and (min-width: 0){html{font-size:14px}}@media only screen and (min-width: 992px){html{font-size:14.5px}}@media only screen and (min-width: 1200px){html{font-size:15px}}h1,h2,h3,h4,h5,h6{font-weight:400;line-height:1.3}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{font-weight:inherit}h1{font-size:4.2rem;line-height:110%;margin:2.8rem 0 1.68rem 0}h2{font-size:3.56rem;line-height:110%;margin:2.3733333333rem 0 1.424rem 0}h3{font-size:2.92rem;line-height:110%;margin:1.9466666667rem 0 1.168rem 0}h4{font-size:2.28rem;line-height:110%;margin:1.52rem 0 .912rem 0}h5{font-size:1.64rem;line-height:110%;margin:1.0933333333rem 0 .656rem 0}h6{font-size:1.15rem;line-height:110%;margin:.7666666667rem 0 .46rem 0}em{font-style:italic}strong{font-weight:500}small{font-size:75%}.light{font-weight:300}.thin{font-weight:200}@media only screen and (min-width: 360px){.flow-text{font-size:1.2rem}}@media only screen and (min-width: 390px){.flow-text{font-size:1.224rem}}@media only screen and (min-width: 420px){.flow-text{font-size:1.248rem}}@media only screen and (min-width: 450px){.flow-text{font-size:1.272rem}}@media only screen and (min-width: 480px){.flow-text{font-size:1.296rem}}@media only screen and (min-width: 510px){.flow-text{font-size:1.32rem}}@media only screen and (min-width: 540px){.flow-text{font-size:1.344rem}}@media only screen and (min-width: 570px){.flow-text{font-size:1.368rem}}@media only screen and (min-width: 600px){.flow-text{font-size:1.392rem}}@media only screen and (min-width: 630px){.flow-text{font-size:1.416rem}}@media only screen and (min-width: 660px){.flow-text{font-size:1.44rem}}@media only screen and (min-width: 690px){.flow-text{font-size:1.464rem}}@media only screen and (min-width: 720px){.flow-text{font-size:1.488rem}}@media only screen and (min-width: 750px){.flow-text{font-size:1.512rem}}@media only screen and (min-width: 780px){.flow-text{font-size:1.536rem}}@media only screen and (min-width: 810px){.flow-text{font-size:1.56rem}}@media only screen and (min-width: 840px){.flow-text{font-size:1.584rem}}@media only screen and (min-width: 870px){.flow-text{font-size:1.608rem}}@media only screen and (min-width: 900px){.flow-text{font-size:1.632rem}}@media only screen and (min-width: 930px){.flow-text{font-size:1.656rem}}@media only screen and (min-width: 960px){.flow-text{font-size:1.68rem}}@media only screen and (max-width: 360px){.flow-text{font-size:1.2rem}}.scale-transition{-webkit-transition:-webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:-webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63), -webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important}.scale-transition.scale-out{-webkit-transform:scale(0);transform:scale(0);-webkit-transition:-webkit-transform .2s !important;transition:-webkit-transform .2s !important;transition:transform .2s !important;transition:transform .2s, -webkit-transform .2s !important}.scale-transition.scale-in{-webkit-transform:scale(1);transform:scale(1)}.card-panel{-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s;padding:24px;margin:.5rem 0 1rem 0;border-radius:2px;background-color:#fff}.card{position:relative;margin:.5rem 0 1rem 0;background-color:#fff;-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s;border-radius:2px}.card .card-title{font-size:24px;font-weight:300}.card .card-title.activator{cursor:pointer}.card.small,.card.medium,.card.large{position:relative}.card.small .card-image,.card.medium .card-image,.card.large .card-image{max-height:60%;overflow:hidden}.card.small .card-image+.card-content,.card.medium .card-image+.card-content,.card.large .card-image+.card-content{max-height:40%}.card.small .card-content,.card.medium .card-content,.card.large .card-content{max-height:100%;overflow:hidden}.card.small .card-action,.card.medium .card-action,.card.large .card-action{position:absolute;bottom:0;left:0;right:0}.card.small{height:300px}.card.medium{height:400px}.card.large{height:500px}.card.horizontal{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.card.horizontal.small .card-image,.card.horizontal.medium .card-image,.card.horizontal.large .card-image{height:100%;max-height:none;overflow:visible}.card.horizontal.small .card-image img,.card.horizontal.medium .card-image img,.card.horizontal.large .card-image img{height:100%}.card.horizontal .card-image{max-width:50%}.card.horizontal .card-image img{border-radius:2px 0 0 2px;max-width:100%;width:auto}.card.horizontal .card-stacked{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;position:relative}.card.horizontal .card-stacked .card-content{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.card.sticky-action .card-action{z-index:2}.card.sticky-action .card-reveal{z-index:1;padding-bottom:64px}.card .card-image{position:relative}.card .card-image img{display:block;border-radius:2px 2px 0 0;position:relative;left:0;right:0;top:0;bottom:0;width:100%}.card .card-image .card-title{color:#fff;position:absolute;bottom:0;left:0;max-width:100%;padding:24px}.card .card-content{padding:24px;border-radius:0 0 2px 2px}.card .card-content p{margin:0}.card .card-content .card-title{display:block;line-height:32px;margin-bottom:8px}.card .card-content .card-title i{line-height:32px}.card .card-action{background-color:inherit;border-top:1px solid rgba(160,160,160,0.2);position:relative;padding:16px 24px}.card .card-action:last-child{border-radius:0 0 2px 2px}.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating){color:#ffab40;margin-right:24px;-webkit-transition:color .3s ease;transition:color .3s ease;text-transform:uppercase}.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating):hover{color:#ffd8a6}.card .card-reveal{padding:24px;position:absolute;background-color:#fff;width:100%;overflow-y:auto;left:0;top:100%;height:100%;z-index:3;display:none}.card .card-reveal .card-title{cursor:pointer;display:block}#toast-container{display:block;position:fixed;z-index:10000}@media only screen and (max-width: 600px){#toast-container{min-width:100%;bottom:0%}}@media only screen and (min-width: 601px) and (max-width: 992px){#toast-container{left:5%;bottom:7%;max-width:90%}}@media only screen and (min-width: 993px){#toast-container{top:10%;right:7%;max-width:86%}}.toast{border-radius:2px;top:35px;width:auto;margin-top:10px;position:relative;max-width:100%;height:auto;min-height:48px;line-height:1.5em;background-color:#323232;padding:10px 25px;font-size:1.1rem;font-weight:300;color:#fff;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;cursor:default}.toast .toast-action{color:#eeff41;font-weight:500;margin-right:-25px;margin-left:3rem}.toast.rounded{border-radius:24px}@media only screen and (max-width: 600px){.toast{width:100%;border-radius:0}}.tabs{position:relative;overflow-x:auto;overflow-y:hidden;height:48px;width:100%;background-color:#fff;margin:0 auto;white-space:nowrap}.tabs.tabs-transparent{background-color:transparent}.tabs.tabs-transparent .tab a,.tabs.tabs-transparent .tab.disabled a,.tabs.tabs-transparent .tab.disabled a:hover{color:rgba(255,255,255,0.7)}.tabs.tabs-transparent .tab a:hover,.tabs.tabs-transparent .tab a.active{color:#fff}.tabs.tabs-transparent .indicator{background-color:#fff}.tabs.tabs-fixed-width{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.tabs.tabs-fixed-width .tab{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.tabs .tab{display:inline-block;text-align:center;line-height:48px;height:48px;padding:0;margin:0;text-transform:uppercase}.tabs .tab a{color:rgba(238,110,115,0.7);display:block;width:100%;height:100%;padding:0 24px;font-size:14px;text-overflow:ellipsis;overflow:hidden;-webkit-transition:color .28s ease, background-color .28s ease;transition:color .28s ease, background-color .28s ease}.tabs .tab a:focus,.tabs .tab a:focus.active{background-color:rgba(246,178,181,0.2);outline:none}.tabs .tab a:hover,.tabs .tab a.active{background-color:transparent;color:#ee6e73}.tabs .tab.disabled a,.tabs .tab.disabled a:hover{color:rgba(238,110,115,0.4);cursor:default}.tabs .indicator{position:absolute;bottom:0;height:2px;background-color:#f6b2b5;will-change:left, right}@media only screen and (max-width: 992px){.tabs{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.tabs .tab{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.tabs .tab a{padding:0 12px}}.material-tooltip{padding:10px 8px;font-size:1rem;z-index:2000;background-color:transparent;border-radius:2px;color:#fff;min-height:36px;line-height:120%;opacity:0;position:absolute;text-align:center;max-width:calc(100% - 4px);overflow:hidden;left:0;top:0;pointer-events:none;visibility:hidden;background-color:#323232}.backdrop{position:absolute;opacity:0;height:7px;width:14px;border-radius:0 0 50% 50%;background-color:#323232;z-index:-1;-webkit-transform-origin:50% 0%;transform-origin:50% 0%;visibility:hidden}.btn,.btn-large,.btn-small,.btn-flat{border:none;border-radius:2px;display:inline-block;height:36px;line-height:36px;padding:0 16px;text-transform:uppercase;vertical-align:middle;-webkit-tap-highlight-color:transparent}.btn.disabled,.disabled.btn-large,.disabled.btn-small,.btn-floating.disabled,.btn-large.disabled,.btn-small.disabled,.btn-flat.disabled,.btn:disabled,.btn-large:disabled,.btn-small:disabled,.btn-floating:disabled,.btn-large:disabled,.btn-small:disabled,.btn-flat:disabled,.btn[disabled],.btn-large[disabled],.btn-small[disabled],.btn-floating[disabled],.btn-large[disabled],.btn-small[disabled],.btn-flat[disabled]{pointer-events:none;background-color:#DFDFDF !important;-webkit-box-shadow:none;box-shadow:none;color:#9F9F9F !important;cursor:default}.btn.disabled:hover,.disabled.btn-large:hover,.disabled.btn-small:hover,.btn-floating.disabled:hover,.btn-large.disabled:hover,.btn-small.disabled:hover,.btn-flat.disabled:hover,.btn:disabled:hover,.btn-large:disabled:hover,.btn-small:disabled:hover,.btn-floating:disabled:hover,.btn-large:disabled:hover,.btn-small:disabled:hover,.btn-flat:disabled:hover,.btn[disabled]:hover,.btn-large[disabled]:hover,.btn-small[disabled]:hover,.btn-floating[disabled]:hover,.btn-large[disabled]:hover,.btn-small[disabled]:hover,.btn-flat[disabled]:hover{background-color:#DFDFDF !important;color:#9F9F9F !important}.btn,.btn-large,.btn-small,.btn-floating,.btn-large,.btn-small,.btn-flat{font-size:14px;outline:0}.btn i,.btn-large i,.btn-small i,.btn-floating i,.btn-large i,.btn-small i,.btn-flat i{font-size:1.3rem;line-height:inherit}.btn:focus,.btn-large:focus,.btn-small:focus,.btn-floating:focus{background-color:#1d7d74}.btn,.btn-large,.btn-small{text-decoration:none;color:#fff;background-color:#26a69a;text-align:center;letter-spacing:.5px;-webkit-transition:background-color .2s ease-out;transition:background-color .2s ease-out;cursor:pointer}.btn:hover,.btn-large:hover,.btn-small:hover{background-color:#2bbbad}.btn-floating{display:inline-block;color:#fff;position:relative;overflow:hidden;z-index:1;width:40px;height:40px;line-height:40px;padding:0;background-color:#26a69a;border-radius:50%;-webkit-transition:background-color .3s;transition:background-color .3s;cursor:pointer;vertical-align:middle}.btn-floating:hover{background-color:#26a69a}.btn-floating:before{border-radius:0}.btn-floating.btn-large{width:56px;height:56px;padding:0}.btn-floating.btn-large.halfway-fab{bottom:-28px}.btn-floating.btn-large i{line-height:56px}.btn-floating.btn-small{width:32.4px;height:32.4px}.btn-floating.btn-small.halfway-fab{bottom:-16.2px}.btn-floating.btn-small i{line-height:32.4px}.btn-floating.halfway-fab{position:absolute;right:24px;bottom:-20px}.btn-floating.halfway-fab.left{right:auto;left:24px}.btn-floating i{width:inherit;display:inline-block;text-align:center;color:#fff;font-size:1.6rem;line-height:40px}button.btn-floating{border:none}.fixed-action-btn{position:fixed;right:23px;bottom:23px;padding-top:15px;margin-bottom:0;z-index:997}.fixed-action-btn.active ul{visibility:visible}.fixed-action-btn.direction-left,.fixed-action-btn.direction-right{padding:0 0 0 15px}.fixed-action-btn.direction-left ul,.fixed-action-btn.direction-right ul{text-align:right;right:64px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);height:100%;left:auto;width:500px}.fixed-action-btn.direction-left ul li,.fixed-action-btn.direction-right ul li{display:inline-block;margin:7.5px 15px 0 0}.fixed-action-btn.direction-right{padding:0 15px 0 0}.fixed-action-btn.direction-right ul{text-align:left;direction:rtl;left:64px;right:auto}.fixed-action-btn.direction-right ul li{margin:7.5px 0 0 15px}.fixed-action-btn.direction-bottom{padding:0 0 15px 0}.fixed-action-btn.direction-bottom ul{top:64px;bottom:auto;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:reverse;-webkit-flex-direction:column-reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.fixed-action-btn.direction-bottom ul li{margin:15px 0 0 0}.fixed-action-btn.toolbar{padding:0;height:56px}.fixed-action-btn.toolbar.active>a i{opacity:0}.fixed-action-btn.toolbar ul{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;top:0;bottom:0;z-index:1}.fixed-action-btn.toolbar ul li{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;display:inline-block;margin:0;height:100%;-webkit-transition:none;transition:none}.fixed-action-btn.toolbar ul li a{display:block;overflow:hidden;position:relative;width:100%;height:100%;background-color:transparent;-webkit-box-shadow:none;box-shadow:none;color:#fff;line-height:56px;z-index:1}.fixed-action-btn.toolbar ul li a i{line-height:inherit}.fixed-action-btn ul{left:0;right:0;text-align:center;position:absolute;bottom:64px;margin:0;visibility:hidden}.fixed-action-btn ul li{margin-bottom:15px}.fixed-action-btn ul a.btn-floating{opacity:0}.fixed-action-btn .fab-backdrop{position:absolute;top:0;left:0;z-index:-1;width:40px;height:40px;background-color:#26a69a;border-radius:50%;-webkit-transform:scale(0);transform:scale(0)}.btn-flat{-webkit-box-shadow:none;box-shadow:none;background-color:transparent;color:#343434;cursor:pointer;-webkit-transition:background-color .2s;transition:background-color .2s}.btn-flat:focus,.btn-flat:hover{-webkit-box-shadow:none;box-shadow:none}.btn-flat:focus{background-color:rgba(0,0,0,0.1)}.btn-flat.disabled,.btn-flat.btn-flat[disabled]{background-color:transparent !important;color:#b3b2b2 !important;cursor:default}.btn-large{height:54px;line-height:54px;font-size:15px;padding:0 28px}.btn-large i{font-size:1.6rem}.btn-small{height:32.4px;line-height:32.4px;font-size:13px}.btn-small i{font-size:1.2rem}.btn-block{display:block}.dropdown-content{background-color:#fff;margin:0;display:none;min-width:100px;overflow-y:auto;opacity:0;position:absolute;left:0;top:0;z-index:9999;-webkit-transform-origin:0 0;transform-origin:0 0}.dropdown-content:focus{outline:0}.dropdown-content li{clear:both;color:rgba(0,0,0,0.87);cursor:pointer;min-height:50px;line-height:1.5rem;width:100%;text-align:left}.dropdown-content li:hover,.dropdown-content li.active{background-color:#eee}.dropdown-content li:focus{outline:none}.dropdown-content li.divider{min-height:0;height:1px}.dropdown-content li>a,.dropdown-content li>span{font-size:16px;color:#26a69a;display:block;line-height:22px;padding:14px 16px}.dropdown-content li>span>label{top:1px;left:0;height:18px}.dropdown-content li>a>i{height:inherit;line-height:inherit;float:left;margin:0 24px 0 0;width:24px}body.keyboard-focused .dropdown-content li:focus{background-color:#dadada}.input-field.col .dropdown-content [type="checkbox"]+label{top:1px;left:0;height:18px;-webkit-transform:none;transform:none}.dropdown-trigger{cursor:pointer}/*! + * Waves v0.6.0 + * http://fian.my.id/Waves + * + * Copyright 2014 Alfiana E. Sibuea and other contributors + * Released under the MIT license + * https://github.com/fians/Waves/blob/master/LICENSE + */.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;-webkit-transition:.3s ease-out;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,0.2);-webkit-transition:all 0.7s ease-out;transition:all 0.7s ease-out;-webkit-transition-property:opacity, -webkit-transform;transition-property:opacity, -webkit-transform;transition-property:transform, opacity;transition-property:transform, opacity, -webkit-transform;-webkit-transform:scale(0);transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:rgba(255,255,255,0.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,0.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,0.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,0.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,0.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,0.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,0.7)}.waves-effect input[type="button"],.waves-effect input[type="reset"],.waves-effect input[type="submit"]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{-webkit-transition:none !important;transition:none !important}.waves-circle{-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle, white 100%, black 100%)}.waves-input-wrapper{border-radius:0.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top, opacity}.modal:focus{outline:none}@media only screen and (max-width: 992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%;text-align:right}.modal .modal-footer .btn,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small,.modal .modal-footer .btn-flat{margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-25%;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,0.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom, opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem 0}.collapsible-header{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;cursor:pointer;-webkit-tap-highlight-color:transparent;line-height:1.5;padding:1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header:focus{outline:0}.collapsible-header i{width:2rem;font-size:1.6rem;display:inline-block;text-align:center;margin-right:1rem}.keyboard-focused .collapsible-header:focus{background-color:#eee}.collapsible-body{display:none;border-bottom:1px solid #ddd;-webkit-box-sizing:border-box;box-sizing:border-box;padding:2rem}.sidenav .collapsible,.sidenav.fixed .collapsible{border:none;-webkit-box-shadow:none;box-shadow:none}.sidenav .collapsible li,.sidenav.fixed .collapsible li{padding:0}.sidenav .collapsible-header,.sidenav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.sidenav .collapsible-header:hover,.sidenav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,0.05)}.sidenav .collapsible-header i,.sidenav.fixed .collapsible-header i{line-height:inherit}.sidenav .collapsible-body,.sidenav.fixed .collapsible-body{border:0;background-color:#fff}.sidenav .collapsible-body li a,.sidenav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;-webkit-box-shadow:none;box-shadow:none}.collapsible.popout>li{-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);margin:0 24px;-webkit-transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)}.collapsible.popout>li.active{-webkit-box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;margin-bottom:5px;margin-right:5px}.chip:focus{outline:none;background-color:#26a69a;color:#fff}.chip>img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip .close{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.chips{border:none;border-bottom:1px solid #9e9e9e;-webkit-box-shadow:none;box-shadow:none;margin:0 0 8px 0;min-height:45px;outline:none;-webkit-transition:all .3s;transition:all .3s}.chips.focus{border-bottom:1px solid #26a69a;-webkit-box-shadow:0 1px 0 0 #26a69a;box-shadow:0 1px 0 0 #26a69a}.chips:hover{cursor:text}.chips .input{background:none;border:0;color:rgba(0,0,0,0.6);display:inline-block;font-size:16px;height:3rem;line-height:32px;outline:0;margin:0;padding:0 !important;width:120px !important}.chips .input:focus{border:0 !important;-webkit-box-shadow:none !important;box-shadow:none !important}.chips .autocomplete-content{margin-top:0;margin-bottom:0}.prefix ~ .chips{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.chips:empty ~ label{font-size:0.8rem;-webkit-transform:translateY(-140%);transform:translateY(-140%)}.materialboxed{display:block;cursor:-webkit-zoom-in;cursor:zoom-in;position:relative;-webkit-transition:opacity .4s;transition:opacity .4s;-webkit-backface-visibility:hidden}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:-webkit-zoom-out;cursor:zoom-out}#materialbox-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#292929;z-index:1000;will-change:opacity}.materialbox-caption{position:fixed;display:none;color:#fff;line-height:50px;bottom:0;left:0;width:100%;text-align:center;padding:0% 15%;height:50px;z-index:1000;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #c9f3ef}button:focus{outline:none;background-color:#2ab7a9}label{font-size:.8rem;color:#9e9e9e}::-webkit-input-placeholder{color:#d1d1d1}::-moz-placeholder{color:#d1d1d1}:-ms-input-placeholder{color:#d1d1d1}::-ms-input-placeholder{color:#d1d1d1}::placeholder{color:#d1d1d1}input:not([type]),input[type=text]:not(.browser-default),input[type=password]:not(.browser-default),input[type=email]:not(.browser-default),input[type=url]:not(.browser-default),input[type=time]:not(.browser-default),input[type=date]:not(.browser-default),input[type=datetime]:not(.browser-default),input[type=datetime-local]:not(.browser-default),input[type=tel]:not(.browser-default),input[type=number]:not(.browser-default),input[type=search]:not(.browser-default),textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:content-box;box-sizing:content-box;-webkit-transition:border .3s, -webkit-box-shadow .3s;transition:border .3s, -webkit-box-shadow .3s;transition:box-shadow .3s, border .3s;transition:box-shadow .3s, border .3s, -webkit-box-shadow .3s}input:not([type]):disabled,input:not([type])[readonly="readonly"],input[type=text]:not(.browser-default):disabled,input[type=text]:not(.browser-default)[readonly="readonly"],input[type=password]:not(.browser-default):disabled,input[type=password]:not(.browser-default)[readonly="readonly"],input[type=email]:not(.browser-default):disabled,input[type=email]:not(.browser-default)[readonly="readonly"],input[type=url]:not(.browser-default):disabled,input[type=url]:not(.browser-default)[readonly="readonly"],input[type=time]:not(.browser-default):disabled,input[type=time]:not(.browser-default)[readonly="readonly"],input[type=date]:not(.browser-default):disabled,input[type=date]:not(.browser-default)[readonly="readonly"],input[type=datetime]:not(.browser-default):disabled,input[type=datetime]:not(.browser-default)[readonly="readonly"],input[type=datetime-local]:not(.browser-default):disabled,input[type=datetime-local]:not(.browser-default)[readonly="readonly"],input[type=tel]:not(.browser-default):disabled,input[type=tel]:not(.browser-default)[readonly="readonly"],input[type=number]:not(.browser-default):disabled,input[type=number]:not(.browser-default)[readonly="readonly"],input[type=search]:not(.browser-default):disabled,input[type=search]:not(.browser-default)[readonly="readonly"],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly="readonly"]{color:rgba(0,0,0,0.42);border-bottom:1px dotted rgba(0,0,0,0.42)}input:not([type]):disabled+label,input:not([type])[readonly="readonly"]+label,input[type=text]:not(.browser-default):disabled+label,input[type=text]:not(.browser-default)[readonly="readonly"]+label,input[type=password]:not(.browser-default):disabled+label,input[type=password]:not(.browser-default)[readonly="readonly"]+label,input[type=email]:not(.browser-default):disabled+label,input[type=email]:not(.browser-default)[readonly="readonly"]+label,input[type=url]:not(.browser-default):disabled+label,input[type=url]:not(.browser-default)[readonly="readonly"]+label,input[type=time]:not(.browser-default):disabled+label,input[type=time]:not(.browser-default)[readonly="readonly"]+label,input[type=date]:not(.browser-default):disabled+label,input[type=date]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime]:not(.browser-default):disabled+label,input[type=datetime]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime-local]:not(.browser-default):disabled+label,input[type=datetime-local]:not(.browser-default)[readonly="readonly"]+label,input[type=tel]:not(.browser-default):disabled+label,input[type=tel]:not(.browser-default)[readonly="readonly"]+label,input[type=number]:not(.browser-default):disabled+label,input[type=number]:not(.browser-default)[readonly="readonly"]+label,input[type=search]:not(.browser-default):disabled+label,input[type=search]:not(.browser-default)[readonly="readonly"]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly="readonly"]+label{color:rgba(0,0,0,0.42)}input:not([type]):focus:not([readonly]),input[type=text]:not(.browser-default):focus:not([readonly]),input[type=password]:not(.browser-default):focus:not([readonly]),input[type=email]:not(.browser-default):focus:not([readonly]),input[type=url]:not(.browser-default):focus:not([readonly]),input[type=time]:not(.browser-default):focus:not([readonly]),input[type=date]:not(.browser-default):focus:not([readonly]),input[type=datetime]:not(.browser-default):focus:not([readonly]),input[type=datetime-local]:not(.browser-default):focus:not([readonly]),input[type=tel]:not(.browser-default):focus:not([readonly]),input[type=number]:not(.browser-default):focus:not([readonly]),input[type=search]:not(.browser-default):focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #26a69a;-webkit-box-shadow:0 1px 0 0 #26a69a;box-shadow:0 1px 0 0 #26a69a}input:not([type]):focus:not([readonly])+label,input[type=text]:not(.browser-default):focus:not([readonly])+label,input[type=password]:not(.browser-default):focus:not([readonly])+label,input[type=email]:not(.browser-default):focus:not([readonly])+label,input[type=url]:not(.browser-default):focus:not([readonly])+label,input[type=time]:not(.browser-default):focus:not([readonly])+label,input[type=date]:not(.browser-default):focus:not([readonly])+label,input[type=datetime]:not(.browser-default):focus:not([readonly])+label,input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label,input[type=tel]:not(.browser-default):focus:not([readonly])+label,input[type=number]:not(.browser-default):focus:not([readonly])+label,input[type=search]:not(.browser-default):focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#26a69a}input:not([type]):focus.valid ~ label,input[type=text]:not(.browser-default):focus.valid ~ label,input[type=password]:not(.browser-default):focus.valid ~ label,input[type=email]:not(.browser-default):focus.valid ~ label,input[type=url]:not(.browser-default):focus.valid ~ label,input[type=time]:not(.browser-default):focus.valid ~ label,input[type=date]:not(.browser-default):focus.valid ~ label,input[type=datetime]:not(.browser-default):focus.valid ~ label,input[type=datetime-local]:not(.browser-default):focus.valid ~ label,input[type=tel]:not(.browser-default):focus.valid ~ label,input[type=number]:not(.browser-default):focus.valid ~ label,input[type=search]:not(.browser-default):focus.valid ~ label,textarea.materialize-textarea:focus.valid ~ label{color:#4CAF50}input:not([type]):focus.invalid ~ label,input[type=text]:not(.browser-default):focus.invalid ~ label,input[type=password]:not(.browser-default):focus.invalid ~ label,input[type=email]:not(.browser-default):focus.invalid ~ label,input[type=url]:not(.browser-default):focus.invalid ~ label,input[type=time]:not(.browser-default):focus.invalid ~ label,input[type=date]:not(.browser-default):focus.invalid ~ label,input[type=datetime]:not(.browser-default):focus.invalid ~ label,input[type=datetime-local]:not(.browser-default):focus.invalid ~ label,input[type=tel]:not(.browser-default):focus.invalid ~ label,input[type=number]:not(.browser-default):focus.invalid ~ label,input[type=search]:not(.browser-default):focus.invalid ~ label,textarea.materialize-textarea:focus.invalid ~ label{color:#F44336}input:not([type]).validate+label,input[type=text]:not(.browser-default).validate+label,input[type=password]:not(.browser-default).validate+label,input[type=email]:not(.browser-default).validate+label,input[type=url]:not(.browser-default).validate+label,input[type=time]:not(.browser-default).validate+label,input[type=date]:not(.browser-default).validate+label,input[type=datetime]:not(.browser-default).validate+label,input[type=datetime-local]:not(.browser-default).validate+label,input[type=tel]:not(.browser-default).validate+label,input[type=number]:not(.browser-default).validate+label,input[type=search]:not(.browser-default).validate+label,textarea.materialize-textarea.validate+label{width:100%}input.valid:not([type]),input.valid:not([type]):focus,input.valid[type=text]:not(.browser-default),input.valid[type=text]:not(.browser-default):focus,input.valid[type=password]:not(.browser-default),input.valid[type=password]:not(.browser-default):focus,input.valid[type=email]:not(.browser-default),input.valid[type=email]:not(.browser-default):focus,input.valid[type=url]:not(.browser-default),input.valid[type=url]:not(.browser-default):focus,input.valid[type=time]:not(.browser-default),input.valid[type=time]:not(.browser-default):focus,input.valid[type=date]:not(.browser-default),input.valid[type=date]:not(.browser-default):focus,input.valid[type=datetime]:not(.browser-default),input.valid[type=datetime]:not(.browser-default):focus,input.valid[type=datetime-local]:not(.browser-default),input.valid[type=datetime-local]:not(.browser-default):focus,input.valid[type=tel]:not(.browser-default),input.valid[type=tel]:not(.browser-default):focus,input.valid[type=number]:not(.browser-default),input.valid[type=number]:not(.browser-default):focus,input.valid[type=search]:not(.browser-default),input.valid[type=search]:not(.browser-default):focus,textarea.materialize-textarea.valid,textarea.materialize-textarea.valid:focus,.select-wrapper.valid>input.select-dropdown{border-bottom:1px solid #4CAF50;-webkit-box-shadow:0 1px 0 0 #4CAF50;box-shadow:0 1px 0 0 #4CAF50}input.invalid:not([type]),input.invalid:not([type]):focus,input.invalid[type=text]:not(.browser-default),input.invalid[type=text]:not(.browser-default):focus,input.invalid[type=password]:not(.browser-default),input.invalid[type=password]:not(.browser-default):focus,input.invalid[type=email]:not(.browser-default),input.invalid[type=email]:not(.browser-default):focus,input.invalid[type=url]:not(.browser-default),input.invalid[type=url]:not(.browser-default):focus,input.invalid[type=time]:not(.browser-default),input.invalid[type=time]:not(.browser-default):focus,input.invalid[type=date]:not(.browser-default),input.invalid[type=date]:not(.browser-default):focus,input.invalid[type=datetime]:not(.browser-default),input.invalid[type=datetime]:not(.browser-default):focus,input.invalid[type=datetime-local]:not(.browser-default),input.invalid[type=datetime-local]:not(.browser-default):focus,input.invalid[type=tel]:not(.browser-default),input.invalid[type=tel]:not(.browser-default):focus,input.invalid[type=number]:not(.browser-default),input.invalid[type=number]:not(.browser-default):focus,input.invalid[type=search]:not(.browser-default),input.invalid[type=search]:not(.browser-default):focus,textarea.materialize-textarea.invalid,textarea.materialize-textarea.invalid:focus,.select-wrapper.invalid>input.select-dropdown,.select-wrapper.invalid>input.select-dropdown:focus{border-bottom:1px solid #F44336;-webkit-box-shadow:0 1px 0 0 #F44336;box-shadow:0 1px 0 0 #F44336}input:not([type]).valid ~ .helper-text[data-success],input:not([type]):focus.valid ~ .helper-text[data-success],input:not([type]).invalid ~ .helper-text[data-error],input:not([type]):focus.invalid ~ .helper-text[data-error],input[type=text]:not(.browser-default).valid ~ .helper-text[data-success],input[type=text]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=text]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=text]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=password]:not(.browser-default).valid ~ .helper-text[data-success],input[type=password]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=password]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=password]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=email]:not(.browser-default).valid ~ .helper-text[data-success],input[type=email]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=email]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=email]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=url]:not(.browser-default).valid ~ .helper-text[data-success],input[type=url]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=url]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=url]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=time]:not(.browser-default).valid ~ .helper-text[data-success],input[type=time]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=time]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=time]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=date]:not(.browser-default).valid ~ .helper-text[data-success],input[type=date]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=date]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=date]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=datetime]:not(.browser-default).valid ~ .helper-text[data-success],input[type=datetime]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=datetime]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=datetime]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=datetime-local]:not(.browser-default).valid ~ .helper-text[data-success],input[type=datetime-local]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=datetime-local]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=datetime-local]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=tel]:not(.browser-default).valid ~ .helper-text[data-success],input[type=tel]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=tel]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=tel]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=number]:not(.browser-default).valid ~ .helper-text[data-success],input[type=number]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=number]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=number]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=search]:not(.browser-default).valid ~ .helper-text[data-success],input[type=search]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=search]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=search]:not(.browser-default):focus.invalid ~ .helper-text[data-error],textarea.materialize-textarea.valid ~ .helper-text[data-success],textarea.materialize-textarea:focus.valid ~ .helper-text[data-success],textarea.materialize-textarea.invalid ~ .helper-text[data-error],textarea.materialize-textarea:focus.invalid ~ .helper-text[data-error],.select-wrapper.valid .helper-text[data-success],.select-wrapper.invalid ~ .helper-text[data-error]{color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none}input:not([type]).valid ~ .helper-text:after,input:not([type]):focus.valid ~ .helper-text:after,input[type=text]:not(.browser-default).valid ~ .helper-text:after,input[type=text]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=password]:not(.browser-default).valid ~ .helper-text:after,input[type=password]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=email]:not(.browser-default).valid ~ .helper-text:after,input[type=email]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=url]:not(.browser-default).valid ~ .helper-text:after,input[type=url]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=time]:not(.browser-default).valid ~ .helper-text:after,input[type=time]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=date]:not(.browser-default).valid ~ .helper-text:after,input[type=date]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=datetime]:not(.browser-default).valid ~ .helper-text:after,input[type=datetime]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default).valid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=tel]:not(.browser-default).valid ~ .helper-text:after,input[type=tel]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=number]:not(.browser-default).valid ~ .helper-text:after,input[type=number]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=search]:not(.browser-default).valid ~ .helper-text:after,input[type=search]:not(.browser-default):focus.valid ~ .helper-text:after,textarea.materialize-textarea.valid ~ .helper-text:after,textarea.materialize-textarea:focus.valid ~ .helper-text:after,.select-wrapper.valid ~ .helper-text:after{content:attr(data-success);color:#4CAF50}input:not([type]).invalid ~ .helper-text:after,input:not([type]):focus.invalid ~ .helper-text:after,input[type=text]:not(.browser-default).invalid ~ .helper-text:after,input[type=text]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=password]:not(.browser-default).invalid ~ .helper-text:after,input[type=password]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=email]:not(.browser-default).invalid ~ .helper-text:after,input[type=email]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=url]:not(.browser-default).invalid ~ .helper-text:after,input[type=url]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=time]:not(.browser-default).invalid ~ .helper-text:after,input[type=time]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=date]:not(.browser-default).invalid ~ .helper-text:after,input[type=date]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=datetime]:not(.browser-default).invalid ~ .helper-text:after,input[type=datetime]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default).invalid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=tel]:not(.browser-default).invalid ~ .helper-text:after,input[type=tel]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=number]:not(.browser-default).invalid ~ .helper-text:after,input[type=number]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=search]:not(.browser-default).invalid ~ .helper-text:after,input[type=search]:not(.browser-default):focus.invalid ~ .helper-text:after,textarea.materialize-textarea.invalid ~ .helper-text:after,textarea.materialize-textarea:focus.invalid ~ .helper-text:after,.select-wrapper.invalid ~ .helper-text:after{content:attr(data-error);color:#F44336}input:not([type])+label:after,input[type=text]:not(.browser-default)+label:after,input[type=password]:not(.browser-default)+label:after,input[type=email]:not(.browser-default)+label:after,input[type=url]:not(.browser-default)+label:after,input[type=time]:not(.browser-default)+label:after,input[type=date]:not(.browser-default)+label:after,input[type=datetime]:not(.browser-default)+label:after,input[type=datetime-local]:not(.browser-default)+label:after,input[type=tel]:not(.browser-default)+label:after,input[type=number]:not(.browser-default)+label:after,input[type=search]:not(.browser-default)+label:after,textarea.materialize-textarea+label:after,.select-wrapper+label:after{display:block;content:"";position:absolute;top:100%;left:0;opacity:0;-webkit-transition:.2s opacity ease-out, .2s color ease-out;transition:.2s opacity ease-out, .2s color ease-out}.input-field{position:relative;margin-top:1rem;margin-bottom:1rem}.input-field.inline{display:inline-block;vertical-align:middle;margin-left:5px}.input-field.inline input,.input-field.inline .select-dropdown{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix ~ label,.input-field.col .prefix ~ .validate ~ label{width:calc(100% - 3rem - 1.5rem)}.input-field>label{color:#9e9e9e;position:absolute;top:0;left:0;font-size:1rem;cursor:text;-webkit-transition:color .2s ease-out, -webkit-transform .2s ease-out;transition:color .2s ease-out, -webkit-transform .2s ease-out;transition:transform .2s ease-out, color .2s ease-out;transition:transform .2s ease-out, color .2s ease-out, -webkit-transform .2s ease-out;-webkit-transform-origin:0% 100%;transform-origin:0% 100%;text-align:initial;-webkit-transform:translateY(12px);transform:translateY(12px)}.input-field>label:not(.label-icon).active{-webkit-transform:translateY(-14px) scale(0.8);transform:translateY(-14px) scale(0.8);-webkit-transform-origin:0 0;transform-origin:0 0}.input-field>input[type]:-webkit-autofill:not(.browser-default):not([type="search"])+label,.input-field>input[type=date]:not(.browser-default)+label,.input-field>input[type=time]:not(.browser-default)+label{-webkit-transform:translateY(-14px) scale(0.8);transform:translateY(-14px) scale(0.8);-webkit-transform-origin:0 0;transform-origin:0 0}.input-field .helper-text{position:relative;min-height:18px;display:block;font-size:12px;color:rgba(0,0,0,0.54)}.input-field .helper-text::after{opacity:1;position:absolute;top:0;left:0}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;-webkit-transition:color .2s;transition:color .2s;top:.5rem}.input-field .prefix.active{color:#26a69a}.input-field .prefix ~ input,.input-field .prefix ~ textarea,.input-field .prefix ~ label,.input-field .prefix ~ .validate ~ label,.input-field .prefix ~ .helper-text,.input-field .prefix ~ .autocomplete-content{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix ~ label{margin-left:3rem}@media only screen and (max-width: 992px){.input-field .prefix ~ input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width: 600px){.input-field .prefix ~ input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;-webkit-transition:.3s background-color;transition:.3s background-color}.nav-wrapper .input-field input[type=search]{height:inherit;padding-left:4rem;width:calc(100% - 4rem);border:0;-webkit-box-shadow:none;box-shadow:none}.input-field input[type=search]:focus:not(.browser-default){background-color:#fff;border:0;-webkit-box-shadow:none;box-shadow:none;color:#444}.input-field input[type=search]:focus:not(.browser-default)+label i,.input-field input[type=search]:focus:not(.browser-default) ~ .mdi-navigation-close,.input-field input[type=search]:focus:not(.browser-default) ~ .material-icons{color:#444}.input-field input[type=search]+.label-icon{-webkit-transform:none;transform:none;left:1rem}.input-field input[type=search] ~ .mdi-navigation-close,.input-field input[type=search] ~ .material-icons{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;-webkit-transition:.3s color;transition:.3s color}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{line-height:normal;overflow-y:hidden;padding:.8rem 0 .8rem 0;resize:none;min-height:3rem;-webkit-box-sizing:border-box;box-sizing:border-box}.hiddendiv{visibility:hidden;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem;position:absolute;top:0;z-index:-1}.autocomplete-content li .highlight{color:#444}.autocomplete-content li img{height:40px;width:40px;margin:5px 15px}.character-counter{min-height:18px}[type="radio"]:not(:checked),[type="radio"]:checked{position:absolute;opacity:0;pointer-events:none}[type="radio"]:not(:checked)+span,[type="radio"]:checked+span{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;-webkit-transition:.28s ease;transition:.28s ease;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}[type="radio"]+span:before,[type="radio"]+span:after{content:'';position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;-webkit-transition:.28s ease;transition:.28s ease}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after,[type="radio"]:checked+span:before,[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border-radius:50%}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after{border:2px solid #5a5a5a}[type="radio"]:not(:checked)+span:after{-webkit-transform:scale(0);transform:scale(0)}[type="radio"]:checked+span:before{border:2px solid transparent}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border:2px solid #26a69a}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:after{background-color:#26a69a}[type="radio"]:checked+span:after{-webkit-transform:scale(1.02);transform:scale(1.02)}[type="radio"].with-gap:checked+span:after{-webkit-transform:scale(0.5);transform:scale(0.5)}[type="radio"].tabbed:focus+span:before{-webkit-box-shadow:0 0 0 10px rgba(0,0,0,0.1);box-shadow:0 0 0 10px rgba(0,0,0,0.1)}[type="radio"].with-gap:disabled:checked+span:before{border:2px solid rgba(0,0,0,0.42)}[type="radio"].with-gap:disabled:checked+span:after{border:none;background-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before,[type="radio"]:disabled:checked+span:before{background-color:transparent;border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled+span{color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before{border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:checked+span:after{background-color:rgba(0,0,0,0.42);border-color:#949494}[type="checkbox"]:not(:checked),[type="checkbox"]:checked{position:absolute;opacity:0;pointer-events:none}[type="checkbox"]+span:not(.lever){position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}[type="checkbox"]+span:not(.lever):before,[type="checkbox"]:not(.filled-in)+span:not(.lever):after{content:'';position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #5a5a5a;border-radius:1px;margin-top:3px;-webkit-transition:.2s;transition:.2s}[type="checkbox"]:not(.filled-in)+span:not(.lever):after{border:0;-webkit-transform:scale(0);transform:scale(0)}[type="checkbox"]:not(:checked):disabled+span:not(.lever):before{border:none;background-color:rgba(0,0,0,0.42)}[type="checkbox"].tabbed:focus+span:not(.lever):after{-webkit-transform:scale(1);transform:scale(1);border:0;border-radius:50%;-webkit-box-shadow:0 0 0 10px rgba(0,0,0,0.1);box-shadow:0 0 0 10px rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.1)}[type="checkbox"]:checked+span:not(.lever):before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #26a69a;border-bottom:2px solid #26a69a;-webkit-transform:rotate(40deg);transform:rotate(40deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"]:checked:disabled+span:before{border-right:2px solid rgba(0,0,0,0.42);border-bottom:2px solid rgba(0,0,0,0.42)}[type="checkbox"]:indeterminate+span:not(.lever):before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #26a69a;border-bottom:none;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"]:indeterminate:disabled+span:not(.lever):before{border-right:2px solid rgba(0,0,0,0.42);background-color:transparent}[type="checkbox"].filled-in+span:not(.lever):after{border-radius:2px}[type="checkbox"].filled-in+span:not(.lever):before,[type="checkbox"].filled-in+span:not(.lever):after{content:'';left:0;position:absolute;-webkit-transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;z-index:1}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;-webkit-transform:rotateZ(37deg);transform:rotateZ(37deg);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):after{height:20px;width:20px;background-color:transparent;border:2px solid #5a5a5a;top:0px;z-index:0}[type="checkbox"].filled-in:checked+span:not(.lever):before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;-webkit-transform:rotateZ(37deg);transform:rotateZ(37deg);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"].filled-in:checked+span:not(.lever):after{top:0;width:20px;height:20px;border:2px solid #26a69a;background-color:#26a69a;z-index:0}[type="checkbox"].filled-in.tabbed:focus+span:not(.lever):after{border-radius:2px;border-color:#5a5a5a;background-color:rgba(0,0,0,0.1)}[type="checkbox"].filled-in.tabbed:checked:focus+span:not(.lever):after{border-radius:2px;background-color:#26a69a;border-color:#26a69a}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):before{background-color:transparent;border:2px solid transparent}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):after{border-color:transparent;background-color:#949494}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):before{background-color:transparent}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):after{background-color:#949494;border-color:#949494}.switch,.switch *{-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#84c7c1}.switch label input[type=checkbox]:checked+.lever:before,.switch label input[type=checkbox]:checked+.lever:after{left:18px}.switch label input[type=checkbox]:checked+.lever:after{background-color:#26a69a}.switch label .lever{content:"";display:inline-block;position:relative;width:36px;height:14px;background-color:rgba(0,0,0,0.38);border-radius:15px;margin-right:10px;-webkit-transition:background 0.3s ease;transition:background 0.3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:before,.switch label .lever:after{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;-webkit-transition:left 0.3s ease, background .3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease;transition:left 0.3s ease, background .3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease}.switch label .lever:before{background-color:rgba(38,166,154,0.15)}.switch label .lever:after{background-color:#F1F1F1;-webkit-box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12);box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)}input[type=checkbox]:checked:not(:disabled) ~ .lever:active::before,input[type=checkbox]:checked:not(:disabled).tabbed:focus ~ .lever::before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(38,166,154,0.15)}input[type=checkbox]:not(:disabled) ~ .lever:active:before,input[type=checkbox]:not(:disabled).tabbed:focus ~ .lever::before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(0,0,0,0.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(0,0,0,0.12)}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#949494}select{display:none}select.browser-default{display:block}select{background-color:rgba(255,255,255,0.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper.valid+label,.select-wrapper.invalid+label{width:100%;pointer-events:none}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;display:block;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;z-index:1}.select-wrapper input.select-dropdown:focus{border-bottom:1px solid #26a69a}.select-wrapper .caret{position:absolute;right:0;top:0;bottom:0;margin:auto 0;z-index:0;fill:rgba(0,0,0,0.87)}.select-wrapper+label{position:absolute;top:-26px;font-size:.8rem}select:disabled{color:rgba(0,0,0,0.42)}.select-wrapper.disabled+label{color:rgba(0,0,0,0.42)}.select-wrapper.disabled .caret{fill:rgba(0,0,0,0.42)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,0.42);cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.select-wrapper i{color:rgba(0,0,0,0.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,0.3);background-color:transparent}body.keyboard-focused .select-dropdown.dropdown-content li:focus{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li:hover{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li.selected{background-color:rgba(0,0,0,0.03)}.prefix ~ .select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix ~ label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,0.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,0.4)}.select-dropdown li.optgroup ~ li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large,.file-field .btn-small{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.file-field input[type=file]::-webkit-file-upload-button{display:none}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;top:10px;left:0;border:none;height:0;width:0;border-radius:50%;background-color:#26a69a;margin-left:7px;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#26a69a;font-size:0;-webkit-transform:rotate(45deg);transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s;-webkit-appearance:none;background-color:#26a69a;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;margin:-5px 0 0 0}.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb{-webkit-box-shadow:0 0 0 10px rgba(38,166,154,0.26);box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]{border:1px solid white}input[type=range]::-moz-range-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-moz-focus-inner{border:0}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s}.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{display:inline-block;font-weight:300;color:#757575;padding-left:16px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:15px;border-left:1px solid #ee6e73}.table-of-contents a.active{font-weight:500;padding-left:14px;border-left:2px solid #ee6e73}.sidenav{position:fixed;width:300px;left:0;top:0;margin:0;-webkit-transform:translateX(-100%);transform:translateX(-100%);height:100%;height:calc(100% + 60px);height:-moz-calc(100%);padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform:translateX(-105%);transform:translateX(-105%)}.sidenav.right-aligned{right:0;-webkit-transform:translateX(105%);transform:translateX(105%);left:auto;-webkit-transform:translateX(100%);transform:translateX(100%)}.sidenav .collapsible{margin:0}.sidenav li{float:none;line-height:48px}.sidenav li.active{background-color:rgba(0,0,0,0.05)}.sidenav li>a{color:rgba(0,0,0,0.87);display:block;font-size:14px;font-weight:500;height:48px;line-height:48px;padding:0 32px}.sidenav li>a:hover{background-color:rgba(0,0,0,0.05)}.sidenav li>a.btn,.sidenav li>a.btn-large,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating{margin:10px 15px}.sidenav li>a.btn,.sidenav li>a.btn-large,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-floating{color:#fff}.sidenav li>a.btn-flat{color:#343434}.sidenav li>a.btn:hover,.sidenav li>a.btn-large:hover,.sidenav li>a.btn-small:hover,.sidenav li>a.btn-large:hover{background-color:#2bbbad}.sidenav li>a.btn-floating:hover{background-color:#26a69a}.sidenav li>a>i,.sidenav li>a>[class^="mdi-"],.sidenav li>a li>a>[class*="mdi-"],.sidenav li>a>i.material-icons{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,0.54)}.sidenav .divider{margin:8px 0 0 0}.sidenav .subheader{cursor:initial;pointer-events:none;color:rgba(0,0,0,0.54);font-size:14px;font-weight:500;line-height:48px}.sidenav .subheader:hover{background-color:transparent}.sidenav .user-view{position:relative;padding:32px 32px 0;margin-bottom:8px}.sidenav .user-view>a{height:auto;padding:0}.sidenav .user-view>a:hover{background-color:transparent}.sidenav .user-view .background{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.sidenav .user-view .circle,.sidenav .user-view .name,.sidenav .user-view .email{display:block}.sidenav .user-view .circle{height:64px;width:64px}.sidenav .user-view .name,.sidenav .user-view .email{font-size:14px;line-height:24px}.sidenav .user-view .name{margin-top:16px;font-weight:500}.sidenav .user-view .email{padding-bottom:16px;font-weight:400}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.drag-target.right-aligned{right:0}.sidenav.sidenav-fixed{left:0;-webkit-transform:translateX(0);transform:translateX(0);position:fixed}.sidenav.sidenav-fixed.right-aligned{right:0;left:auto}@media only screen and (max-width: 992px){.sidenav.sidenav-fixed{-webkit-transform:translateX(-105%);transform:translateX(-105%)}.sidenav.sidenav-fixed.right-aligned{-webkit-transform:translateX(105%);transform:translateX(105%)}.sidenav>a{padding:0 16px}.sidenav .user-view{padding:16px 16px 0}}.sidenav .collapsible-body>ul:not(.collapsible)>li.active,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active{background-color:#ee6e73}.sidenav .collapsible-body>ul:not(.collapsible)>li.active a,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active a{color:#fff}.sidenav .collapsible-body{padding:0}.sidenav-overlay{position:fixed;top:0;left:0;right:0;opacity:0;height:120vh;background-color:rgba(0,0,0,0.5);z-index:997;display:none}.preloader-wrapper{display:inline-block;position:relative;width:50px;height:50px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(360deg)}}@keyframes container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#26a69a}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only,.active .spinner-layer.spinner-green-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg)}}@keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@-webkit-keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}@keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent !important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent !important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent !important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes left-spin{from{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes right-spin{from{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1);animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1)}@-webkit-keyframes fade-out{from{opacity:1}to{opacity:0}}@keyframes fade-out{from{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:center}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;-webkit-transition:background-color .3s;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4CAF50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;-webkit-perspective:500px;perspective:500px;-webkit-transform-style:preserve-3d;transform-style:preserve-3d;-webkit-transform-origin:0% 50%;transform-origin:0% 50%}.carousel.carousel-slider{top:0;left:0}.carousel.carousel-slider .carousel-fixed-item{position:absolute;left:0;right:0;bottom:20px;z-index:1}.carousel.carousel-slider .carousel-fixed-item.with-indicators{bottom:68px}.carousel.carousel-slider .carousel-item{width:100%;height:100%;min-height:400px;position:absolute;top:0;left:0}.carousel.carousel-slider .carousel-item h2{font-size:24px;font-weight:500;line-height:32px}.carousel.carousel-slider .carousel-item p{font-size:15px}.carousel .carousel-item{visibility:hidden;width:200px;height:200px;position:absolute;top:0;left:0}.carousel .carousel-item>img{width:100%}.carousel .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.carousel .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:8px;width:8px;margin:24px 4px;background-color:rgba(255,255,255,0.5);-webkit-transition:background-color .3s;transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:#fff}.carousel.scrolling .carousel-item .materialboxed,.carousel .carousel-item:not(.active) .materialboxed{pointer-events:none}.tap-target-wrapper{width:800px;height:800px;position:fixed;z-index:1000;visibility:hidden;-webkit-transition:visibility 0s .3s;transition:visibility 0s .3s}.tap-target-wrapper.open{visibility:visible;-webkit-transition:visibility 0s;transition:visibility 0s}.tap-target-wrapper.open .tap-target{-webkit-transform:scale(1);transform:scale(1);opacity:.95;-webkit-transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-wrapper.open .tap-target-wave::before{-webkit-transform:scale(1);transform:scale(1)}.tap-target-wrapper.open .tap-target-wave::after{visibility:visible;-webkit-animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;-webkit-transition:opacity .3s, visibility 0s 1s, -webkit-transform .3s;transition:opacity .3s, visibility 0s 1s, -webkit-transform .3s;transition:opacity .3s, transform .3s, visibility 0s 1s;transition:opacity .3s, transform .3s, visibility 0s 1s, -webkit-transform .3s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#ee6e73;-webkit-box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);width:100%;height:100%;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave::before,.tap-target-wave::after{content:'';display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#ffffff}.tap-target-wave::before{-webkit-transform:scale(0);transform:scale(0);-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s, -webkit-transform .3s}.tap-target-wave::after{visibility:hidden;-webkit-transition:opacity .3s, visibility 0s, -webkit-transform .3s;transition:opacity .3s, visibility 0s, -webkit-transform .3s;transition:opacity .3s, transform .3s, visibility 0s;transition:opacity .3s, transform .3s, visibility 0s, -webkit-transform .3s;z-index:-1}.tap-target-origin{top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);z-index:10002;position:absolute !important}.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small),.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small):hover{background:none}@media only screen and (max-width: 600px){.tap-target,.tap-target-wrapper{width:600px;height:600px}}.pulse{overflow:visible;position:relative}.pulse::before{content:'';display:block;position:absolute;width:100%;height:100%;top:0;left:0;background-color:inherit;border-radius:inherit;-webkit-transition:opacity .3s, -webkit-transform .3s;transition:opacity .3s, -webkit-transform .3s;transition:opacity .3s, transform .3s;transition:opacity .3s, transform .3s, -webkit-transform .3s;-webkit-animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;z-index:-1}@-webkit-keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}100%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}@keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}100%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}.datepicker-modal{max-width:325px;min-width:300px;max-height:none}.datepicker-container.modal-content{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:0}.datepicker-controls{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;width:280px;margin:0 auto}.datepicker-controls .selects-container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.datepicker-controls .select-wrapper input{border-bottom:none;text-align:center;margin:0}.datepicker-controls .select-wrapper input:focus{border-bottom:none}.datepicker-controls .select-wrapper .caret{display:none}.datepicker-controls .select-year input{width:50px}.datepicker-controls .select-month input{width:70px}.month-prev,.month-next{margin-top:4px;cursor:pointer;background-color:transparent;border:none}.datepicker-date-display{-webkit-box-flex:1;-webkit-flex:1 auto;-ms-flex:1 auto;flex:1 auto;background-color:#26a69a;color:#fff;padding:20px 22px;font-weight:500}.datepicker-date-display .year-text{display:block;font-size:1.5rem;line-height:25px;color:rgba(255,255,255,0.7)}.datepicker-date-display .date-text{display:block;font-size:2.8rem;line-height:47px;font-weight:500}.datepicker-calendar-container{-webkit-box-flex:2.5;-webkit-flex:2.5 auto;-ms-flex:2.5 auto;flex:2.5 auto}.datepicker-table{width:280px;font-size:1rem;margin:0 auto}.datepicker-table thead{border-bottom:none}.datepicker-table th{padding:10px 5px;text-align:center}.datepicker-table tr{border:none}.datepicker-table abbr{text-decoration:none;color:#999}.datepicker-table td{border-radius:50%;padding:0}.datepicker-table td.is-today{color:#26a69a}.datepicker-table td.is-selected{background-color:#26a69a;color:#fff}.datepicker-table td.is-outside-current-month,.datepicker-table td.is-disabled{color:rgba(0,0,0,0.3);pointer-events:none}.datepicker-day-button{background-color:transparent;border:none;line-height:38px;display:block;width:100%;border-radius:50%;padding:0 5px;cursor:pointer;color:inherit}.datepicker-day-button:focus{background-color:rgba(43,161,150,0.25)}.datepicker-footer{width:280px;margin:0 auto;padding-bottom:5px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.datepicker-cancel,.datepicker-clear,.datepicker-today,.datepicker-done{color:#26a69a;padding:0 1rem}.datepicker-clear{color:#F44336}@media only screen and (min-width: 601px){.datepicker-modal{max-width:625px}.datepicker-container.modal-content{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.datepicker-date-display{-webkit-box-flex:0;-webkit-flex:0 1 270px;-ms-flex:0 1 270px;flex:0 1 270px}.datepicker-controls,.datepicker-table,.datepicker-footer{width:320px}.datepicker-day-button{line-height:44px}}.timepicker-modal{max-width:325px;max-height:none}.timepicker-container.modal-content{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:0}.text-primary{color:#fff}.timepicker-digital-display{-webkit-box-flex:1;-webkit-flex:1 auto;-ms-flex:1 auto;flex:1 auto;background-color:#26a69a;padding:10px;font-weight:300}.timepicker-text-container{font-size:4rem;font-weight:bold;text-align:center;color:rgba(255,255,255,0.6);font-weight:400;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.timepicker-span-hours,.timepicker-span-minutes,.timepicker-span-am-pm div{cursor:pointer}.timepicker-span-hours{margin-right:3px}.timepicker-span-minutes{margin-left:3px}.timepicker-display-am-pm{font-size:1.3rem;position:absolute;right:1rem;bottom:1rem;font-weight:400}.timepicker-analog-display{-webkit-box-flex:2.5;-webkit-flex:2.5 auto;-ms-flex:2.5 auto;flex:2.5 auto}.timepicker-plate{background-color:#eee;border-radius:50%;width:270px;height:270px;overflow:visible;position:relative;margin:auto;margin-top:25px;margin-bottom:5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.timepicker-canvas,.timepicker-dial{position:absolute;left:0;right:0;top:0;bottom:0}.timepicker-minutes{visibility:hidden}.timepicker-tick{border-radius:50%;color:rgba(0,0,0,0.87);line-height:40px;text-align:center;width:40px;height:40px;position:absolute;cursor:pointer;font-size:15px}.timepicker-tick.active,.timepicker-tick:hover{background-color:rgba(38,166,154,0.25)}.timepicker-dial{-webkit-transition:opacity 350ms, -webkit-transform 350ms;transition:opacity 350ms, -webkit-transform 350ms;transition:transform 350ms, opacity 350ms;transition:transform 350ms, opacity 350ms, -webkit-transform 350ms}.timepicker-dial-out{opacity:0}.timepicker-dial-out.timepicker-hours{-webkit-transform:scale(1.1, 1.1);transform:scale(1.1, 1.1)}.timepicker-dial-out.timepicker-minutes{-webkit-transform:scale(0.8, 0.8);transform:scale(0.8, 0.8)}.timepicker-canvas{-webkit-transition:opacity 175ms;transition:opacity 175ms}.timepicker-canvas line{stroke:#26a69a;stroke-width:4;stroke-linecap:round}.timepicker-canvas-out{opacity:0.25}.timepicker-canvas-bearing{stroke:none;fill:#26a69a}.timepicker-canvas-bg{stroke:none;fill:#26a69a}.timepicker-footer{margin:0 auto;padding:5px 1rem;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.timepicker-clear{color:#F44336}.timepicker-close{color:#26a69a}.timepicker-clear,.timepicker-close{padding:0 20px}@media only screen and (min-width: 601px){.timepicker-modal{max-width:600px}.timepicker-container.modal-content{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.timepicker-text-container{top:32%}.timepicker-display-am-pm{position:relative;right:auto;bottom:auto;text-align:center;margin-top:1.2rem}} diff --git a/browser/static/css/navigation.css b/browser/static/css/navigation.css deleted file mode 100644 index 933bccf31..000000000 --- a/browser/static/css/navigation.css +++ /dev/null @@ -1,45 +0,0 @@ - -.navbar { - /* Font */ - font-weight: 500; - font-family: "Roboto", "Helvetica", "Arial", sans-serif; - /* */ - color: #ffffff; - background-color: #3f51b5; - width: 100%; - display: flex; - z-index: 1100; - box-sizing: border-box; - flex-shrink: 0; - flex-direction: row; - height: 5em; - justify-content: space-between; - box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); -} - -.logo, .nav-link { - width: 100px; -} - -.logo { - font-size: 1.5em; - margin-top: 1em; - margin-left: 1em; - order: 1; - align-content: flex-start; -} - -.nav-link { -} - -.buttons { - float: right; - order: 2; - margin-top: 2em; - align-content: flex-end; - margin-right: 1em; -} - -.nav-link:last-child{ - margin-left: 1em; -} \ No newline at end of file diff --git a/browser/static/css/normalize.css b/browser/static/css/normalize.css index fb89b52b9..192eb9ce4 100644 --- a/browser/static/css/normalize.css +++ b/browser/static/css/normalize.css @@ -1,12 +1,349 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + body { - margin: 0; - font-family: "Roboto", "Helvetica", "Arial", sans-serif; - /* font */ - color: rgba(0, 0, 0, 0.54); - font-weight: 500; - line-height: 1.16667em; - /*text-transform: uppercase;*/ - /* */ - color: rgba(0, 0, 0, 0.54); - background-color: white; -} \ No newline at end of file + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} diff --git a/browser/static/favicon.ico b/browser/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..93ca78bfdad96e731685f30931f77c0efe683c75 GIT binary patch literal 15086 zcmds;YmAOH|-Vglh3`A!oxG0{Xmh!H*@Mq-RY|D1)-T0)> zo_F5&T%L2zb3dn{&}E?+p_^|GQO*pl{9-8d^-w4@b7u1Q^`X#d-d#_r?~h&)3cW~$ zFHwgkp*f)5JNSZMl=|1qt^>aZz6#cY7r=7E5FOToe*^CYKUXT9nf2*Ibk7DW!B2x` zy3Goj%TDlT;FRV;yIaAH;28M7f;a zznbUug_~rlF*aac?@{y4eQaKMvn5(LSfZr@T%W`W^IA8$?{>b)M#o~-$aK~pH2W(S zW!YAG`gVs&vwQYsoYJW~=w?V(=#PWnU zSVMiC&3bjze)Njj<15UbeADd5ubF-KC9{VqSMO!JM)MDw_H~fw`@k2{-WB&a+6SH6 z99>#>uloSJa@uSJ(Hg@{i=Pq@5@QH>%4^?Y9{VzOrozIfj@bF(xFtL@{yBQJCo8s_ z{qSY8-@IeiE6R!k`5N1bs~Xhu%!R>?OLwjZy zVEQa=n>umc7Wy)gwV#PEwNtVQc_A7zTf&gvflpst*-%UgX0EHD>oO!XZ2f$r1FU#z#(#H7yy2&bhX}j`I8Q zy7v3~A$b9QyUOf$YfA1pYlAT~?PKVQmAjLl`;Pm(kIr;5nzdDdm)Zwt_QD^zy-3Xk zeP;b9Z0HE1`TO^uQtyEH>NMq*~}6)-BQUp)0e=U%tL_e}m~|3t1bl6}f1|{&f@PdE;`C zyKVk2BSbHAmYcn|PxYeCF1hu`?v@>(*V%I?-{@xTPZ>bGIQKm|8g5Un&jvH>Z!Q=Z z7_f$h20M2SS@nw4uR&*{{mAzgpO>7D4L(f0+*Xeayd1aQ<^yhT7_a!j6KQ*lt|$j|5skg{Zo~!}@fDCSFHgVWaM)g1vcwiGTxbjC z&$r4-bV@0JPVhQD@pVLx%5J>C)G1!Gig24{B?1HcJ7~ z-=3sj_N`pdQ|r^8OK-|w!|#uVcUW!Je{AE2YpkZG#-h;@!wO;r@u$-f{SD=!-n>Xa z|E{ch-|IWZnDCbP3rs`~F}COYFzsK_5bbsTkzxrK4;9@oe?e; zT+u(&Qf=$@MV&9RQ0j{9DFzg~xpfeqjv;vEf{|{wE~3o_PG$6pUF$yR=9kcS>`&w2 z-FExDn9Y3B?5n>vt1BimAU_+w#H~{|PFI>=moZ?;KNE?#T?pf|$agqfHF<3m(;yr2 zX{0~K2D_clddoAu{p&20l|g=d9){rqiK2ZV%~k%f zd;)mU*>7?biai5+>u8VEOIwD82WzvxZMrV5s?q4ecp`U-kQ5t(M(M2_0~I{ zQtaaR%lT-2TrXaC{EpAy^|2nAdz!oV;7d^(rM$o+%gr_`WQ?C5moxFZd^z-<^C`5( zl67>qFAtj>eUfPli&pkcv6tvQSLMgW((}$H7DY-xcTmC^#lfFGGWTF4S!lL5P@nSI z;a8XQk?g@!K3}oE$^l{XY&dK!9YZ#2spn!U#nNR283>rTcu?LSEAbS=pn(?F)Wl!&(Z}v8>ME`Jhf+Fb5H+OJo z_Gj`rYcY8_>Sb?T^xIdd`3;b)OYxwXej{pb_A=IXso8=GGvz20C#)fr@ZvfWdy9Nr z%kRuIblQVw-roaaT&68x^lVN*Z)VB2%fQ*-bew}|e*&D5Nq72hzH1IY0C7sD4ai!~ z0`t~*g|0Gez&!2=QF{6~Hx!Di z{AMT=Re4h=)UOha+pO}cP^elZ+!&Pe^qW)tpw!R*2bhHtC%6SHr&lh0@i8cw&#LQqeb*Z3_f$q`@P+kj)M+$@TN6NG4?uX(!&P`l=Kebr7ngMSF?+5ey*vfs7TlCtu z`#A>Xh;)BLs9a0rpf!ds*m6AgKYn2L7c{)iHCys?lJ?;zU;+P_942`hmw(mxJiB;S zb02DRak|d_PaWQATWSaG5NV%1#PKWOcEz?E>JZ-C(hH>)MEyKY1f5wAU-0wJ?kYViNq8``Dcui8tR4OOWBx7(AMj(<)$5wr@nQEBPVb^r#!oI zggUbxrE!ucbT+OyGzVl|8Rg{{G2}X8*Wq^ZAZNFd<0~MViI|^HQw~jcPdfL)Rn2xH zJYvthOLNw;SkSuZoPBTvLb~_4oLG=M&D)3O?DBd?-JJ@pTi3}Q_Q9?mcdsRH+q92e zc(l*q84V6ICvwhSC(NvK=~6&4GYEf@?~k|dB=3Uk772<~buQWzMqRck4+zg^KyPoa zb#``GB2i#rm5bG#4&`P`fPZ~rR=w*O<4@*2ox{HM)?2o0*)nTxZqDB;y2_0|m?s~b zrH9Vm5$kHGLPE6mEJzx3cXcrKQd_)uu{#e=`!xAc{Gj=ldgiMfp!m3#HAn~UiHx=F zuoHXMTEp?f7K|O$4U^|lIxDfZO?ITQ%ichX=hL&gi{s8c zlvgbUkR_MHm+Yt7@{Zi6?%Bk-XZHNce*63{)9$cj_OJ5`%|T}(Q?OA7j&)gIgKu9_ zJfQP+@e(?&jc7u7G@WBl0H+7I3#0pOI&ae*$|=CY3eV~e9rw>gbnh~&8z`HMp%YEA zk>u>%oiy=|#>PfX6<*#%PWmMMN6>-F?Q@1`y2lq|4VaTV3t*1Mw^*~zh?4n#*-4$T z%U382ITgFkB;Kf^1Km;U+Uxk(oiAulMUQNM*G}70bHdhcZzpZfcsi#j{;WjGi%De% z-TlPmoB~KFSLAEg%GSyjy}r%A=cn_s zK2Z7dSD5}Yt8gHuHnxUUd~r9JS2(o}$}7vS(t13LJ(YdZ-Jgrdf5SvB#<`l3UJHZn zI_j)I`C;X{FA_eY{S8SU_%@!|+)D;BhM9NGf6BYO`%ot8ByW#{6YlEsdj=>PN;DL- zC?4cQJ_cSn9(|2J@m_J*t>9V^MN>9FDYzKt4$4&AH9qphC+b`SN-q8hNY2P?;d;ubRG_8AD@fOpUEPh0+o; z!Bn2&3Vvh(=5QtW5QtM`7>1mnZ#dvizTN+8;rR;iS70eTF3b8fsBs?zCmIy#d7Ur2 zGwKni69x%?q$koq<2?)#k)?s|<8e2|owMSjB)Se*-SH0Fc*txM*gkQjJ+u@LIcOyG z>u$jYxVwVM5QhHZot*yF&*SF&k;G_0QFVPF2cZ(cSHyI@?#@ zVVX-O==`zjFrDZOSpFEnrTpP|JWz$JTo>j_ko7l-fZ>NoPFi*uK=Ns)9 z3%VLx;_g(+*T$D|{zDF*t@ey(ryGycmN{sS&>3ewV_n>Hm4B%Fag*c4ls-{xTX|#@ z7l!t%b+qfK?Wzr1SQqsZ67`7;#0%|2W%?{+f44Bq_G1aFU@H{&=ZW(