diff --git a/ricgraph_explorer/ricgraph_explorer.py b/ricgraph_explorer/ricgraph_explorer.py
index 15db83d..2fa05dd 100644
--- a/ricgraph_explorer/ricgraph_explorer.py
+++ b/ricgraph_explorer/ricgraph_explorer.py
@@ -31,14 +31,28 @@
# This file is Ricgraph explorer, a web based tool to access nodes in
# Ricgraph.
# The purpose is to illustrate how web based access using Flask can be done.
-# To keep it simple, everything has been done in this file. It can be improved
-# in many ways, but I didn't do that, since it is meant for research purposes,
+# To keep it simple, everything has been done in this file.
+# Please note that this code is meant for research purposes,
# not for production use. That means, this code has not been hardened for
# "the outside world". Be careful if you expose it to the outside world.
# Original version Rik D.T. Janssen, January 2023.
# Extended Rik D.T. Janssen, February 2023.
#
# ########################################################################
+#
+# For table sorting. Ricgraph explorer uses sorttable.js.
+# It is copied from https://www.kryogenix.org/code/browser/sorttable
+# on February 1, 2023. At that link, you'll find a how-to. It is licensed under X11-MIT.
+# It is renamed to ricgraph_sorttable.js since it has a small modification
+# related to case-insensitive sorting. The script is included in html_body_end.
+#
+# ##############################################################################
+#
+# Ricgraph explorer uses W3.CSS, a modern, responsive, mobile first CSS framework.
+# See https://www.w3schools.com/w3css/default.asp.
+#
+# ##############################################################################
+
import urllib.parse
from typing import Union
@@ -50,54 +64,112 @@
ricgraph_explorer = Flask(__name__)
# When we do a query, we return at most this number of nodes.
-MAX_RESULTS = 75
+MAX_RESULTS = 50
+
+# The style for the buttons, note the space before and after the text.
+button_style = ' w3-button uu-yellow w3-round-large w3-mobile '
+# A button with a black line around it.
+button_style_border = button_style + ' w3-border rj-border-black '
# The html stylesheet.
stylesheet = ''
+# The html preamble
+html_preamble = ''
+html_preamble += ''
+html_preamble += ''
+
+# The html page header.
+page_header = ''
+page_header += '
'
+page_header += ''
+
# The html page footer.
-footer = '
'
-# For table sorting. sorttable.js is copied from https://www.kryogenix.org/code/browser/sorttable
-# on February 1, 2023. At that link, you'll find a how-to. It is licensed under X11-MIT.
-# It is renamed to ricgraph_sorttable.js since it has a small modification.
-footer += ''
-
-# The html search form.
-search_form = ''
-
-searchcontains_form = ''
+page_footer = ''
+
+# The first part of the html page, up to stylesheet and page_header.
+html_body_start = ''
+html_body_start += ''
+html_body_start += html_preamble
+html_body_start += 'Ricgraph explorer'
+html_body_start += ''
+html_body_start += stylesheet
+html_body_start += page_header
+
+# The last part of the html page, from page_footer to script inclusion.
+html_body_end = page_footer
+html_body_end += ''
+html_body_end += ''
+html_body_end += ''
+
+# The html search form for an exact match search (on /search).
+search_form = ''
+search_form += '
'
+search_form += '
'
+search_form += '
Type something to search
'
+search_form += 'This is an case-sensitive, exact match search, using AND if you use multiple fields:'
+search_form += '
'
+search_form += '
'
+search_form += ''
+search_form += '
'
+search_form += '
'
+search_form += ''
+
+# The html search form for a search on a string (on /searchcontains).
+searchcontains_form = ''
+searchcontains_form += '
'
+searchcontains_form += '
'
+searchcontains_form += '
Type something to search
'
+searchcontains_form += 'This is a case-insensitive, inexact match:'
+searchcontains_form += '
'
+searchcontains_form += ''
# ##############################################################################
@@ -109,18 +181,20 @@ def index_html() -> str:
:return: html to be rendered.
"""
- global stylesheet, footer
+ global html_body_start, html_body_end
- html = stylesheet
- html += '
This is Ricgraph explorer. You can use it to explore Ricgraph.'
+ html = html_body_start
+ html += get_html_for_cardstart()
+ html += 'This is Ricgraph explorer. You can use it to explore Ricgraph.'
html += '
'
- html += footer
+ html += get_html_for_cardend()
+ html += html_body_end
return html
@@ -136,9 +210,9 @@ def search(id_value=None) -> str:
:param id_value: value to search for in the 'value' field.
:return: html to be rendered.
"""
- global stylesheet, footer, search_form
+ global html_body_start, html_body_end, search_form
- html = stylesheet
+ html = html_body_start
if request.method == 'POST':
search_name = request.form['search_name']
search_category = request.form['search_category']
@@ -157,7 +231,7 @@ def search(id_value=None) -> str:
html += find_nodes_in_ricgraph(name='',
category='',
value=escape(id_value))
- html += footer
+ html += html_body_end
return html
@@ -170,9 +244,9 @@ def searchcontains() -> str:
:return: html to be rendered.
"""
- global stylesheet, footer, searchcontains_form
+ global html_body_start, html_body_end, searchcontains_form
- html = stylesheet
+ html = html_body_start
if request.method == 'POST':
search_value = request.form['search_value']
html += find_nodes_in_ricgraph(value=escape(search_value),
@@ -180,7 +254,7 @@ def searchcontains() -> str:
else:
html += searchcontains_form
- html += footer
+ html += html_body_end
return html
@@ -189,7 +263,7 @@ def searchcontains() -> str:
# ##############################################################################
def faceted_navigation_in_ricgraph(nodes: list,
name: str = '', category: str = '', value: str = '') -> str:
- """Do faceted navigation in Ricgraph.
+ """Do facet navigation in Ricgraph.
The facets will be constructed based on 'name' and 'category'.
Facets chosen will be "catched" in function search().
If there is only one facet (for either one or both), it will not be shown.
@@ -198,7 +272,8 @@ def faceted_navigation_in_ricgraph(nodes: list,
:param name: name of the nodes to find.
:param category: category of the nodes to find.
:param value: value of the nodes to find.
- :return: html to be rendered.
+ :return: html to be rendered, or empty string ('') if faceted navigation is
+ not useful because there is only one facet.
"""
if len(nodes) == 0:
return ''
@@ -217,42 +292,58 @@ def faceted_navigation_in_ricgraph(nodes: list,
category_histogram[node['category']] += 1
if len(name_histogram) <= 1 and len(category_histogram) <= 1:
- # Faceted navigation does not make sense, don't show facets.
+ # We have only one facet, so don't show the facet panel.
return ''
- faceted_form = '
'
+ faceted_form += '
'
+ faceted_form += get_html_for_cardend()
html = faceted_form
return html
@@ -284,8 +375,10 @@ def find_nodes_in_ricgraph(name: str = '', category: str = '', value: str = '',
return 'Ricgraph could not be opened.'
if name == '' and category == '' and value == '':
- html = '
Please enter a value in the search field.'
- html += ' ' + 'Try again' + '.'
+ html = get_html_for_cardstart()
+ html += 'The search field should have a value.'
+ html += ' ' + 'Please try again' + '.'
+ html += get_html_for_cardend()
return html
if name_want is None:
@@ -295,8 +388,10 @@ def find_nodes_in_ricgraph(name: str = '', category: str = '', value: str = '',
if use_contain_phrase:
if len(value) < 3:
- html = '
Please use at least three characters for your search string.'
- html += ' ' + 'Try again' + '.'
+ html = get_html_for_cardstart()
+ html += 'The search string should be at least three characters.'
+ html += ' ' + 'Please try again' + '.'
+ html += get_html_for_cardend()
return html
result = rcg.read_all_nodes_containing(value=value)
@@ -304,109 +399,132 @@ def find_nodes_in_ricgraph(name: str = '', category: str = '', value: str = '',
result = rcg.read_all_nodes(name=name, category=category, value=value)
if len(result) == 0:
- html = '
Ricgraph explorer could not find anything.'
- html += ' ' + 'Try again' + '.'
+ html = get_html_for_cardstart()
+ html += 'Ricgraph explorer could not find anything.'
+ html += ' ' + 'Please try again' + '.'
+ html += get_html_for_cardend()
return html
- html = '
You searched for:
'
+ html = get_html_for_cardstart()
+ html += 'You searched for:
'
if not use_contain_phrase:
- html += '
name: "' + str(name) + '"'
- html += '
category: "' + str(category) + '"'
+ html += '
name: "' + str(name) + '"'
+ html += '
category: "' + str(category) + '"'
- html += '
value: "' + str(value) + '"'
+ html += '
value: "' + str(value) + '"'
if len(name_want) > 0:
- html += '
name_want: "' + str(name_want) + '"'
+ html += '
name_want: "' + str(name_want) + '"'
if len(category_want) > 0:
- html += '
category_want: "' + str(category_want) + '"'
html += '
'
+ html += get_html_for_cardend()
if use_contain_phrase:
- header = '
Choose one node to continue:'
- html += get_html_table_from_nodes(nodes=result, header_html=header)
+ table_header = 'Choose one node to continue:'
+ html += get_html_table_from_nodes(nodes=result, table_header=table_header)
return html
if len(result) > MAX_RESULTS:
- html += '
There are ' + str(len(result)) + ' results, showing first '
+ html += get_html_for_cardstart()
+ html += 'There are ' + str(len(result)) + ' results, showing first '
html += str(MAX_RESULTS) + '. '
+ html += get_html_for_cardend()
count = 0
+ # Loop over the nodes found.
for node in result:
- # Loop over the nodes found.
count += 1
if count > MAX_RESULTS:
break
- html += '
Ricgraph explorer found node:'
- html += get_html_for_tablestart()
- html += get_html_for_tableheader()
- html += get_html_for_tablerow(node=node)
- html += get_html_for_tableend()
+ html += get_html_for_cardline()
+ html += get_html_table_from_nodes(nodes=[node],
+ table_header='Ricgraph explorer found node:')
if node['category'] == 'person':
personroot_nodes = rcg.get_all_personroot_nodes(node)
if len(personroot_nodes) == 0:
- html += '
No person-root node found, this should not happen.'
+ html += get_html_for_cardstart()
+ html += 'No person-root node found, this should not happen.'
+ html += get_html_for_cardend()
return html
elif len(personroot_nodes) == 1:
- header = '
This is a person node, '
- header += 'these are all IDs of its person-root node:'
+ table_header = 'This is a person node, '
+ table_header += 'these are all IDs of its person-root node:'
neighbor_nodes = rcg.get_all_neighbor_nodes_person(node)
html += get_html_table_from_nodes(nodes=neighbor_nodes,
- header_html=header)
-
- header = '
These are all the neighbors of this person-root node '
- header += '(without person nodes):'
- neighbor_nodes = rcg.get_all_neighbor_nodes(personroot_nodes[0],
- name_want=name_want,
- category_want=category_want,
- category_dontwant='person')
+ table_header=table_header)
- html += get_html_table_from_nodes(nodes=neighbor_nodes,
- header_html=header)
- html += faceted_navigation_in_ricgraph(nodes=neighbor_nodes,
- name=name,
- category=category,
- value=value)
+ table_header = 'These are all the neighbors of this person-root node '
+ table_header += '(without person nodes):'
+ node_to_find_neighbors = personroot_nodes[0]
+ category_dontwant = 'person'
+ # And now fall through.
else:
# More than one person-root node, that should not happen, but it did.
- header = '
There is more than one person-root node '
- header += 'for the node we have found. '
- header += 'This should not happen, but it did, and that is probably caused '
- header += 'by a mislabeling in a source system we harvested. '
- header += 'Choose one person-root node to continue:'
+ table_header = 'There is more than one person-root node '
+ table_header += 'for the node found. '
+ table_header += 'This should not happen, but it did, and that may have been '
+ table_header += 'caused by a mislabeling in a source system we harvested. '
+ table_header += 'Choose one person-root node to continue:'
html += get_html_table_from_nodes(nodes=personroot_nodes,
- header_html=header)
- break
+ table_header=table_header)
+ return html
+ else:
+ table_header = 'These are the neighbors of this node:'
+ node_to_find_neighbors = node
+ category_dontwant = ''
+
+ neighbor_nodes = rcg.get_all_neighbor_nodes(node=node_to_find_neighbors,
+ name_want=name_want,
+ category_want=category_want,
+ category_dontwant=category_dontwant)
+ faceted_html = faceted_navigation_in_ricgraph(nodes=neighbor_nodes,
+ name=name,
+ category=category,
+ value=value)
+ if faceted_html == '':
+ table_header += ' [Facet panel not shown because there is only one facet to show.]'
+
+ table_html = get_html_table_from_nodes(nodes=neighbor_nodes,
+ table_header=table_header)
+ if faceted_html == '':
+ # Faceted navigation not useful, don't show the panel.
+ html += table_html
else:
- header = '
These are the neighbors of this node:'
- neighbor_nodes = rcg.get_all_neighbor_nodes(node,
- name_want=name_want,
- category_want=category_want)
- html += get_html_table_from_nodes(nodes=neighbor_nodes,
- header_html=header)
- html += faceted_navigation_in_ricgraph(nodes=neighbor_nodes,
- name=name,
- category=category,
- value=value)
+ # Divide space between facet panel and table.
+ html += '
'
+ html += '
'
+ html += faceted_html
+ html += '
'
+ html += '
'
+ html += table_html
+ html += '
'
+ html += '
'
+
return html
-def get_html_table_from_nodes(nodes: Union[list, NodeMatch, None], header_html: str = '') -> str:
+def get_html_table_from_nodes(nodes: Union[list, NodeMatch, None], table_header: str = '') -> str:
"""Create a html table for all nodes in the list.
:param nodes: the nodes to create a table from.
- :param header_html: the html to show above the table.
+ :param table_header: the html to show above the table.
:return: html to be rendered.
"""
if len(nodes) == 0:
- return '
No neighbors found.'
+ html = get_html_for_cardstart()
+ html += 'No neighbors found.'
+ html += get_html_for_cardend()
+ return html
- html = header_html
+ html = get_html_for_cardstart()
+ html += table_header
html += get_html_for_tablestart()
html += get_html_for_tableheader()
for node in nodes:
html += get_html_for_tablerow(node=node)
html += get_html_for_tableend()
+ html += get_html_for_cardend()
return html
@@ -418,7 +536,7 @@ def get_html_for_tablestart() -> str:
:return: html to be rendered.
"""
- html = '
'
+ html = '
'
return html
@@ -427,7 +545,7 @@ def get_html_for_tableheader() -> str:
:return: html to be rendered.
"""
- html = '
'
+ html = '
'
html += '
name
'
html += '
category
'
html += '
value
'
@@ -491,6 +609,49 @@ def get_html_for_tableend() -> str:
return html
+# ##############################################################################
+# The HTML for the W3CSS cards is generated here.
+# ##############################################################################
+def get_html_for_cardstart() -> str:
+ """Get the html required for the start of a W3.CSS 'card'.
+ W3.CSS is a modern, responsive, mobile first CSS framework.
+
+ :return: html to be rendered.
+ """
+ html = ''
+ html += '
'
+ html += '
'
+ return html
+
+
+def get_html_for_cardend() -> str:
+ """Get the html required for the end of a W3.CSS 'card'.
+ W3.CSS is a modern, responsive, mobile first CSS framework.
+
+ :return: html to be rendered.
+ """
+ html = '
'
+ html += '
'
+ html += ''
+ return html
+
+
+def get_html_for_cardline() -> str:
+ """Get the html required for a yellow line. It is a W3.CSS 'card'
+ filled with the color 'yellow'.
+
+ :return: html to be rendered.
+ """
+ html = ''
+ html += '
'
+ # Adjust the height of the line is padding-top + padding-bottom.
+ html += '
'
+ html += '
'
+ html += '
'
+ html += ''
+ return html
+
+
# ############################################
# ################### main ###################
# ############################################
@@ -498,7 +659,6 @@ def get_html_for_tableend() -> str:
# For normal use:
ricgraph_explorer.run(port=3030)
-
# For debug purposes:
# ricgraph_explorer.run(debug=True, port=3030)
diff --git a/ricgraph_explorer/static/0-readme.txt b/ricgraph_explorer/static/0-readme.txt
new file mode 100644
index 0000000..638af89
--- /dev/null
+++ b/ricgraph_explorer/static/0-readme.txt
@@ -0,0 +1,24 @@
+Files in this directory are copies from files existing on the web.
+They are here for performance reasons: it is faster to serve them from
+here than to get them from the web.
+
+
+File: ricgraph_sorttable.js
+From: https://www.kryogenix.org/code/browser/sorttable
+Date copied: February 1, 2023
+Changes: changed sorting to sort case-insensitively
+
+
+File: w3pro.css
+From: https://www.w3schools.com/w3css/4/w3pro.css
+Date copied: February 15, 2023
+Changes: none
+Note: w3pro.css is identical to the standard version except for
+ it has no colors defined, but it is smaller (and loads faster).
+
+
+File: UU logo
+From: https://www.uu.nl/organisatie/huisstijl/downloads/logo
+Date copied: February 15, 2023
+Changes: crop & resize
+
diff --git a/ricgraph_explorer/static/uu_logo_orig.png b/ricgraph_explorer/static/uu_logo_orig.png
new file mode 100644
index 0000000..b0efa06
Binary files /dev/null and b/ricgraph_explorer/static/uu_logo_orig.png differ
diff --git a/ricgraph_explorer/static/uu_logo_small.png b/ricgraph_explorer/static/uu_logo_small.png
new file mode 100644
index 0000000..ab30d2e
Binary files /dev/null and b/ricgraph_explorer/static/uu_logo_small.png differ
diff --git a/ricgraph_explorer/static/w3pro.css b/ricgraph_explorer/static/w3pro.css
new file mode 100644
index 0000000..486628a
--- /dev/null
+++ b/ricgraph_explorer/static/w3pro.css
@@ -0,0 +1,150 @@
+/* W3PRO.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
+html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
+/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
+html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
+article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
+audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
+audio:not([controls]){display:none;height:0}[hidden],template{display:none}
+a{background-color:transparent}a:active,a:hover{outline-width:0}
+abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
+b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#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}figure{margin:1em 40px}img{border-style:none}
+code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
+button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
+button,input{overflow:visible}button,select{text-transform:none}
+button,[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{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
+legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
+[type=checkbox],[type=radio]{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-decoration{-webkit-appearance:none}
+::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
+/* End extract */
+html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
+h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
+.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
+h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
+hr{border:0;border-top:1px solid #eee;margin:20px 0}
+.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
+.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
+.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
+.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
+.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
+.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
+.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
+.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
+.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
+.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
+.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
+.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
+.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
+.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
+.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
+.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
+.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
+.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
+.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
+.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
+.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
+.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
+.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
+.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
+.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
+.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
+.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
+.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
+.w3-main,#main{transition:margin-left .4s}
+.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
+.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
+.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
+.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
+.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
+.w3-bar .w3-button{white-space:normal}
+.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
+.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
+.w3-responsive{display:block;overflow-x:auto}
+.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
+.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
+.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
+.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
+.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
+.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
+@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
+.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
+.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
+@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
+.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
+.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
+.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
+.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
+.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
+.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
+.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
+@media (max-width:1205px){.w3-auto{max-width:95%}}
+@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
+.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
+.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
+.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
+@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
+@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
+@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
+@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
+.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
+.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
+.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
+.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
+.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
+.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
+.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
+.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
+.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
+.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
+.w3-display-position{position:absolute}
+.w3-circle{border-radius:50%}
+.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
+.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
+.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
+.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
+.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
+.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
+.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
+.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
+.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
+.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
+.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
+.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
+.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
+.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
+.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
+.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
+.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
+.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
+.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
+.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
+.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
+.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
+.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
+.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
+.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
+.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
+.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
+.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
+.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
+.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
+.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
+.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
+.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
+.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
+.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
+.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
+.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
+.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
+.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
+.w3-left{float:left!important}.w3-right{float:right!important}
+.w3-button:hover{color:#000!important;background-color:#ccc!important}
+.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
+.w3-hover-none:hover{box-shadow:none!important}
\ No newline at end of file