Merge ~rmescandon/snappy-hwe-snaps/+git/wifi-connect:config-ui into ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect:master

Proposed by Roberto Mier Escandon
Status: Merged
Approved by: James Jesudason
Approved revision: 8923330dafb4f905456e11fcfc499a398dcf0a72
Merged at revision: bdaa24398ed18ad69af8e7bb967f91d159092df4
Proposed branch: ~rmescandon/snappy-hwe-snaps/+git/wifi-connect:config-ui
Merge into: ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect:master
Diff against target: 2408 lines (+1779/-192)
24 files modified
.gitignore (+3/-1)
README.md (+40/-0)
daemon/daemon_test.go (+4/-0)
gulpfile.js (+5/-1)
netman/dbus.go (+18/-2)
package.json (+4/-4)
server/handlers.go (+96/-16)
server/handlers_test.go (+24/-0)
server/middleware.go (+2/-28)
server/router.go (+2/-1)
snap/scriptlets/country-codes (+243/-0)
snap/scriptlets/fetch_country_codes.sh (+10/-0)
snap/scriptlets/fill_country_codes.sh (+39/-0)
snap/snapcraft.yaml (+1/-0)
static/css/application.css (+1/-1)
static/sass/application.scss (+9/-5)
static/templates/connecting.html (+6/-3)
static/templates/management.html (+302/-73)
static/templates/operational.html (+65/-47)
utils/config.go (+235/-0)
utils/config_test.go (+543/-0)
utils/utils.go (+10/-1)
wifiap/wifiap.go (+21/-1)
wifiap/wifiap_test.go (+96/-8)
Reviewer Review Type Date Requested Status
System Enablement Bot continuous-integration Approve
Kyle Nitzsche (community) Approve
Konrad Zapałowicz (community) code Needs Fixing
Sheila Miguez (community) Approve
Review via email: mp+326286@code.launchpad.net

Commit message

config-ui initial development

Description of the change

First time management portal is accessed, configuration is asked for.
If there is an existing config file having NoResetCredentials as true, that config is not shown and used existing one instead

- Backend config support including
* read/write local config, stored as $SNAP_COMMON/config.json
* read/write remote config, set using wifi-ap.config exposed rest api
* considered all config read/write atomic operations with rollback support
* wide testing suite

- New section for config in management page
* allow user setting Portal and WiFi settings in different sections
* password - confirm password validation
* got existing remote configuration populating input fields
* show errors in page
* new 'saving' page when success

- Improved login validation
* using vanilla-fw
* skipping not needed additional info in error messages
* Enter is enabled to send password

- Applied vanilla-framework look&feel in all portals

- Updated handlers in server to fit needs

To post a comment you must log in.
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :

Just in case you want to test this, if "wifi.country-code" param is still not available in wifi-ap snap in store, you can apply this temporary patch to skip it:

https://pastebin.canonical.com/191841/

Revision history for this message
Sheila Miguez (codersquid) wrote :

I only have a few minor comments, see below.

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :

Replied inline

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Sheila Miguez (codersquid) wrote :

On Tue, Jun 27, 2017 at 6:17 AM, Roberto Mier Escandón  <
<email address hidden>> wrote:

> Values are taken from http://geotags.com/iso3166/countries.html in
> compilation time by a scriptlet in snap/scriptlets/fill_country_code.sh.
> Then they are transformed to be the select options and inserted in
> management.html replacing
>

I wonder if we should get it live all the time? Country codes don't change
very often do they? Maybe we could run it periodically to generate a static
list file in our repo. The test failure had me thinking of it.

Tests failed due to
../../../snap/scriptlets/fill_country_codes.sh: curl: not found

Command '['/bin/sh', '/tmp/tmpwyb7k31n', '/tmp/tmp9f5mhfj1']' returned
non-zero exit status 127

--
<email address hidden>

Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :

The issue here is not adding curl as build-package for assets part in snapcraft.yaml.
But yes, you are right that the codes don't change unless a new country is created/destroyed (every 3 months or so that a new war begins :P).
That first stage of downloading codes can be executed manually and store results in a file used in compilation time.

Let me change it and populate README.md with steps to execute it

Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :

@Sheila code updated

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Sheila Miguez (codersquid) wrote :

Once you fix the failing test (maybe you forgot to check in the file), I approve.

review: Approve
Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :

Oh, I didn't want to include country_code file there (the got from remote), but I'm afraid ci needs it :)

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Kyle Nitzsche (knitzsche) wrote :

Having only run the code so far, some issues pop up:
- the user is only required to reset passphrase and password, so can we mark those two fields with an asterisk to indicate required onload?
- I think you can remove: "You need to set these config values before continuing:"
- The passphrase "Error: Value is too short. Must have 13 characters at least" is not correct. 8>= len(passphrase) <= 63 (or <=64 if hex, but so far we don't support hex)
- Please also change the error message to read "Error: the passphrase is too..." because "value" is unclear and it is not clear whether it applies to the passphrase or the confirm field
- can we provide a pre-populated selection list for Operation mode (like we do for channel)?
- county code: if you click into the empty country code box, then mouse over the list, then press the 'u' key, a country that does not start with 'u' is selected
- remove the "show operational portal" checkbox. By design, this can only be disabled by preconfiguration by system integrators, not at runtime
- after configuration everything and hitting apply, I got "could not save configuration, server error"

review: Needs Fixing
Revision history for this message
Konrad Zapałowicz (kzapalowicz) wrote :

Minor wording to adjust.

I'm still thinking about reading the local & remote code part. Especially about introducing it as a Reader and Writer interfaces. Not sure yet however if it would play nice with the rest of the code and it is pretty local.

review: Needs Fixing (code)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :

> Having only run the code so far, some issues pop up:
> - the user is only required to reset passphrase and password, so can we mark
> those two fields with an asterisk to indicate required onload?
What happens with blank fields (imagine leaving SSID field empty). Should I not set that setting or should I delete it?. Think that when config page is loaded, wifi-ap current values populate the fields, so there shouldn't be any empty except password ones.
My opinion is that all them are required and the value provided here is the value to set when apply configuration button is pressed.

> - I think you can remove: "You need to set these config values before
> continuing:"
Removed

> - The passphrase "Error: Value is too short. Must have 13 characters at least"
> is not correct. 8>= len(passphrase) <= 63 (or <=64 if hex, but so far we
> don't support hex)
Changed

> - Please also change the error message to read "Error: the passphrase is
> too..." because "value" is unclear and it is not clear whether it applies to
> the passphrase or the confirm field
That was a generic method, that's the reason for having "value" instead of password or passphrase. I've added an additional parameter to customize error message depending on the input fields.

> - can we provide a pre-populated selection list for Operation mode (like we do
> for channel)?
Good idea. Done

> - county code: if you click into the empty country code box, then mouse over
> the list, then press the 'u' key, a country that does not start with 'u' is
> selected
Weird. Repeating steps i get Uganda. If I continue pressing u I see switch amongst other countries starting with u

> - remove the "show operational portal" checkbox. By design, this can only be
> disabled by preconfiguration by system integrators, not at runtime
Done

> - after configuration everything and hitting apply, I got "could not save
> configuration, server error"
I've updated error message to show more specific messages

Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :

@Konrad, Replied/fixed your comments.

Related with using readers and writers for the config. Looks like another valid aproach, but that would require some changes that I'm not sure if they worth...

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Kyle Nitzsche (knitzsche) wrote :
Download full text (3.2 KiB)

On 06/29/2017 12:17 PM, Roberto Mier Escandón  wrote:
>> Having only run the code so far, some issues pop up:
>> - the user is only required to reset passphrase and password, so can we mark
>> those two fields with an asterisk to indicate required onload?
> What happens with blank fields (imagine leaving SSID field empty). Should I not set that setting or should I delete it?. Think that when config page is loaded, wifi-ap current values populate the fields, so there shouldn't be any empty except password ones.
> My opinion is that all them are required and the value provided here is the value to set when apply configuration button is pressed.

Right, all fields should be pre-populated with current values (except
password and passphrase and country code if country code is not yet set)

On first use, the user must reset passphrase and password, so these I
think should have an asterisk in this First Use case to indicate they
are required.

Form validation should not accept any blanks fields *except* for
passphrase and password IF they have already reset the creds OR
no-reset-creds is true 9screenly case). So SSID should never be empty.

Run time params should ONLY be reset if they are different (to prevent
unnecessary wifi-ap Down > UP.

Therefore the message at the bottom should say:
This may take down the device Wi-Fi AP and disconnect you. Please
reconnect in a minute or two

Also:

Please change the title "Access Point" to "Wi-Fi Access Point"

And change the title "Portal" to "Web Portal"

>
>> - I think you can remove: "You need to set these config values before
>> continuing:"
> Removed
>

thx

>> - The passphrase "Error: Value is too short. Must have 13 characters at least"
>> is not correct. 8>= len(passphrase) <= 63 (or <=64 if hex, but so far we
>> don't support hex)
> Changed
>

thx

>> - Please also change the error message to read "Error: the passphrase is
>> too..." because "value" is unclear and it is not clear whether it applies to
>> the passphrase or the confirm field
> That was a generic method, that's the reason for having "value" instead of password or passphrase. I've added an additional parameter to customize error message depending on the input fields.

thx

>> - can we provide a pre-populated selection list for Operation mode (like we do
>> for channel)?
> Good idea. Done

awesome

>
>> - county code: if you click into the empty country code box, then mouse over
>> the list, then press the 'u' key, a country that does not start with 'u' is
>> selected
> Weird. Repeating steps i get Uganda. If I continue pressing u I see switch amongst other countries starting with u

this issue is reproducible for me, are you skipping the mouse-over step?

>
>> - remove the "show operational portal" checkbox. By design, this can only be
>> disabled by preconfiguration by system integrators, not at runtime
> Done

great

>
>> - after configuration everything and hitting apply, I got "could not save
>> configuration, server error"
> I've updated error message to show more specific messages
>

Yes, now I see "Error:Error saving config: Error writing remote
configuration: wifi-ap set operation failed: "Failed: Invalid ke...

Read more...

Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :

Ready to re-review.

The issue with combo box, tomorrow share your screen for me to see it. I think I'm not doing same steps.

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Kyle Nitzsche (knitzsche) wrote :

Adding some notes from external conversation:

config content should be always available (not just on first use)

config content should show on first use of management portal and be available from management portal at any later time

config content should physically be part of management.html (not in a separate html page). Visibility of parts is controlled by css appropriately.

Just to be clear, pre-config.json should NEVER be written to since it represents the system-integrator's pre config via gadget, if any

The only value you need to read from pre-config.json is portal.no-reset-creds. If this is true, then the user is not required to reset the portal password and the wifi passphrase on first use.

I don't think we need to write any config.json. Any changed values should be set after form validates in wifi-ap (and the portal password if that is set)

All params on the config page except for portal.password should get their values for display from wifi-ap (no local storage of these values)

Emtpy fields (except for portal password and wifi passprhase) should cause form validation error.

After form validates, only changed params should be set. This prevents wifi-ap DOWN>UP if not needed.

review: Needs Fixing
Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :
Download full text (3.5 KiB)

>
>
> On 06/29/2017 12:17 PM, Roberto Mier Escandón  wrote:
> >> Having only run the code so far, some issues pop up:
> >> - the user is only required to reset passphrase and password, so can we
> mark
> >> those two fields with an asterisk to indicate required onload?
> > What happens with blank fields (imagine leaving SSID field empty). Should I
> not set that setting or should I delete it?. Think that when config page is
> loaded, wifi-ap current values populate the fields, so there shouldn't be any
> empty except password ones.
> > My opinion is that all them are required and the value provided here is the
> value to set when apply configuration button is pressed.
>
> Right, all fields should be pre-populated with current values (except
> password and passphrase and country code if country code is not yet set)
>
> On first use, the user must reset passphrase and password, so these I
> think should have an asterisk in this First Use case to indicate they
> are required.
>
> Form validation should not accept any blanks fields *except* for
> passphrase and password IF they have already reset the creds OR
> no-reset-creds is true 9screenly case). So SSID should never be empty.

SSID and interface validate if they are blank when saving configuration.

>
> Run time params should ONLY be reset if they are different (to prevent
> unnecessary wifi-ap Down > UP.
They are only set if they are different. Before setting, old config is read

>
> Therefore the message at the bottom should say:
> This may take down the device Wi-Fi AP and disconnect you. Please
> reconnect in a minute or two
>
Changed

>
> Also:
>
> Please change the title "Access Point" to "Wi-Fi Access Point"
>
> And change the title "Portal" to "Web Portal"
>
Changed

> >
> >> - I think you can remove: "You need to set these config values before
> >> continuing:"
> > Removed
> >
>
> thx
>
> >> - The passphrase "Error: Value is too short. Must have 13 characters at
> least"
> >> is not correct. 8>= len(passphrase) <= 63 (or <=64 if hex, but so far we
> >> don't support hex)
> > Changed
> >
>
> thx
>
> >> - Please also change the error message to read "Error: the passphrase is
> >> too..." because "value" is unclear and it is not clear whether it applies
> to
> >> the passphrase or the confirm field
> > That was a generic method, that's the reason for having "value" instead of
> password or passphrase. I've added an additional parameter to customize error
> message depending on the input fields.
>
> thx
>
> >> - can we provide a pre-populated selection list for Operation mode (like we
> do
> >> for channel)?
> > Good idea. Done
>
> awesome
>
> >
> >> - county code: if you click into the empty country code box, then mouse
> over
> >> the list, then press the 'u' key, a country that does not start with 'u' is
> >> selected
> > Weird. Repeating steps i get Uganda. If I continue pressing u I see switch
> amongst other countries starting with u
>
> this issue is reproducible for me, are you skipping the mouse-over step?
>
> >
> >> - remove the "show operational portal" checkbox. By design, this can only
> be
> >> disabled by preconfiguration by system integra...

Read more...

Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :

> Adding some notes from external conversation:
>
> config content should be always available (not just on first use)
This PR focus on first config

>
> config content should show on first use of management portal and be available
> from management portal at any later time
That has to be in another PR. This one only covers first config. Otherwise this PR can last forever adding new features not required at beginning.

>
> config content should physically be part of management.html (not in a separate
> html page). Visibility of parts is controlled by css appropriately.
I don't see the reason for this. Actually I'm completely against having all the portal in only one html template. However, so it is made.

>
> Just to be clear, pre-config.json should NEVER be written to since it
> represents the system-integrator's pre config via gadget, if any
It is not written in any case

>
> The only value you need to read from pre-config.json is portal.no-reset-creds.
> If this is true, then the user is not required to reset the portal password
> and the wifi passphrase on first use.
It is also reading portal.no-operational, though it's not been used so far anywhere

>
> I don't think we need to write any config.json. Any changed values should be
> set after form validates in wifi-ap (and the portal password if that is set)
There is no config.json

>
> All params on the config page except for portal.password should get their
> values for display from wifi-ap (no local storage of these values)
All remote values are taken from wifi-ap, local ones from pre-config.json, though in config page any local one is populated

>
> Emtpy fields (except for portal password and wifi passprhase) should cause
> form validation error.
They do

>
> After form validates, only changed params should be set. This prevents wifi-ap
> DOWN>UP if not needed.
So it works

Revision history for this message
Kyle Nitzsche (knitzsche) wrote :

> > Adding some notes from external conversation:
> >
> > config content should be always available (not just on first use)
> This PR focus on first config
>
>
> >
> > config content should show on first use of management portal and be
> available
> > from management portal at any later time
> That has to be in another PR. This one only covers first config. Otherwise
> this PR can last forever adding new features not required at beginning.
>

Are you saying this PR only displays config UI on first use? That's not the overall spec'd behavior. In any case, if you are planning to leave out normal configuration after first use, please create a trello card for this.

>
> >
> > config content should physically be part of management.html (not in a
> separate
> > html page). Visibility of parts is controlled by css appropriately.
> I don't see the reason for this. Actually I'm completely against having all
> the portal in only one html template. However, so it is made.

Can you please provide your reasoning?

>
>
>
> >
> > Just to be clear, pre-config.json should NEVER be written to since it
> > represents the system-integrator's pre config via gadget, if any
> It is not written in any case
>
> >
> > The only value you need to read from pre-config.json is portal.no-reset-
> creds.
> > If this is true, then the user is not required to reset the portal password
> > and the wifi passphrase on first use.
> It is also reading portal.no-operational, though it's not been used so far
> anywhere

Can you drop the no-operational part? (You may have added that when you thought we wanted a Checkbox to not display the oper portal, but really that's never going to be configured by this UI.

>
> >
> > I don't think we need to write any config.json. Any changed values should be
> > set after form validates in wifi-ap (and the portal password if that is set)
> There is no config.json
>
>
> >
> > All params on the config page except for portal.password should get their
> > values for display from wifi-ap (no local storage of these values)
> All remote values are taken from wifi-ap, local ones from pre-config.json,
> though in config page any local one is populated

Which local ones are you populating?

thx
>
>
> >
> > Emtpy fields (except for portal password and wifi passprhase) should cause
> > form validation error.
> They do
>
> >
> > After form validates, only changed params should be set. This prevents wifi-
> ap
> > DOWN>UP if not needed.
> So it works

Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :
Download full text (4.4 KiB)

> > > Adding some notes from external conversation:
> > >
> > > config content should be always available (not just on first use)
> > This PR focus on first config
> >
> >
> > >
> > > config content should show on first use of management portal and be
> > available
> > > from management portal at any later time
> > That has to be in another PR. This one only covers first config. Otherwise
> > this PR can last forever adding new features not required at beginning.
> >
>
> Are you saying this PR only displays config UI on first use? That's not the
> overall spec'd behavior. In any case, if you are planning to leave out normal
> configuration after first use, please create a trello card for this.

This is the card related with this PR "As KyleN I want to have access to a simple web UI so that a user can configure the SSID and password for the AP" (That's the title. Description is empty)

From a simple web UI, we have moved to a complete configuration portal, including SSID, wifi passphrase, interface, country code (dynamically populated from remote content), channel and portal password; input verification of not empty fields, length for passwords, password and confirmation be equals, switch to official look&feel framework, replaced js alerts to notification divs, complete config backend support including a large tests suite, red asterisks on password fields, additional message div when saving config, removal of crackable login messages...

and you pretend that also a button to show config in management portal and rendering a different config page (with no red asterisks), behaving different (not requesting changing password or passphrase) should be included in this MP?

If you want to complete project in this MP just be clear and let me know. Is the overall specification applying only to this MP? I don't think so.

If you want to add another card in backlog, do it yourself. I'm not in this project on monday, I should have left it one week ago and, for helping and trying to finish my pending tasks, I've been warned about it today. So, whatever it takes to you.

>
> >
> > >
> > > config content should physically be part of management.html (not in a
> > separate
> > > html page). Visibility of parts is controlled by css appropriately.
> > I don't see the reason for this. Actually I'm completely against having all
> > the portal in only one html template. However, so it is made.
>
> Can you please provide your reasoning?

Is it really needed explaining why having a complete web with several pages in only one html file is crazy?
Is this like explaining what's the meaning of a method name?
are you kidding?
>
> >
> >
> >
> > >
> > > Just to be clear, pre-config.json should NEVER be written to since it
> > > represents the system-integrator's pre config via gadget, if any
> > It is not written in any case
> >
> > >
> > > The only value you need to read from pre-config.json is portal.no-reset-
> > creds.
> > > If this is true, then the user is not required to reset the portal
> password
> > > and the wifi passphrase on first use.
> > It is also reading portal.no-operational, though it's not been used so far
> > anywhere
>
> Can you drop the no-opera...

Read more...

Revision history for this message
Kyle Nitzsche (knitzsche) wrote :

Hi Roberto,

Thanks for the PR.

One reason to keep the config ui in the management.html page is that after form validation, we could display your "saving config... you need to reconnect" message immediately (show the div, hide the others), whereas with this PR, that message displayed for me *after* the wifi-ap went down and I reconnected, at which time it makes no sense. I'll log this as bug.

Given the time constraints, please make one change: in wifiap.go, please delete the wifi.country-code param at the top of the Set() func, and add a //TODO comment saying this needs to be removed when wifiap.go supports it. (I think you said it is not yet supported). Otherwise, the PR is broken and you can't get passed the config page.)

Revision history for this message
Kyle Nitzsche (knitzsche) wrote :

Also, the management page design has changed, and regressed I think. I filed this bug: https://bugs.launchpad.net/snappy-hwe-snaps/+bug/1701729

Here's the bug I mentioned above about saving config message being displayed too late:
https://bugs.launchpad.net/snappy-hwe-snaps/+bug/1701727

Revision history for this message
Kyle Nitzsche (knitzsche) wrote :

Alfonso said the country will land and be published soon, so please merge this as is if possible.

review: Approve
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2index b923a21..41bf8bd 100644
3--- a/.gitignore
4+++ b/.gitignore
5@@ -1,3 +1,6 @@
6+!/.gitignore
7+*.snap
8+*.tar.bz2
9 # testing files
10 .coverage
11 .tests-extras
12@@ -11,4 +14,3 @@ parts/
13 prime/
14 snap/.snapcraft/
15 stage/
16-*.snap
17diff --git a/README.md b/README.md
18index 1759797..e30f47d 100644
19--- a/README.md
20+++ b/README.md
21@@ -265,6 +265,46 @@ Restart the daemon normal loop cleanly with:
22 sudo wifi-connect start
23 ```
24
25+# Configuration
26+
27+First time one access management portal a configuration page is shown. Once there, settings for Wi-Fi access point and portals password can be saved.
28+That configuration is composed of one first section for Wi-Fi ones and another second for portal
29+
30+*Wi-Fi config*:
31+| Config Key | Description |
32+|----------------|-------------|
33+| SSID | of the access point |
34+| Passphrase | password to connect to access point |
35+| Interface | network interface where access point must be brought up |
36+| Country Code | ISO-3166 two digits code to use in Wi-Fi |
37+| Channel | Wi-Fi channel |
38+| Operation Mode | Wi-Fi operation mode |
39+
40+*Portal config*:
41+| Config Key | Description |
42+|----------------|-------------|
43+| Password | to access any of management or operational portals |
44+
45+There is a button to apply configuration just below the input fields. While configuration
46+is being saved a different page will inform about changes are in process to be saved. After small delay one
47+should be able to reconnect to AP in order to see regular management page showing available ssids.
48+
49+NOTE: If there is any previous config file at $SNAP_COMMON/config.json containining 'portal.no-reset-creds' key with value true, first time accessing
50+management portal won't show config page to modify it.
51+
52+## Country codes
53+
54+There is a combo box in config page to select the country code to use in the access point.
55+Its values are injected in compilation time from a file stored at snap/scriptlets/country-codes.
56+This file is generated dynamically when executed by hand this script:
57+
58+```
59+snap/scriptlets/fetch_country_codes.sh
60+```
61+
62+So, every time you want to update country codes to most recent ones, you have to execute
63+that script by hand and, after success, rebuild and install snap
64+
65 # Tests
66 ## Unit Tests
67
68diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go
69index 177c26b..b7d0199 100644
70--- a/daemon/daemon_test.go
71+++ b/daemon/daemon_test.go
72@@ -163,6 +163,10 @@ func (mock *mockWifiap) SetPassphrase(p string) error {
73 return nil
74 }
75
76+func (mock *mockWifiap) Set(map[string]interface{}) error {
77+ return nil
78+}
79+
80 func TestLoadPreConfig(t *testing.T) {
81 PreConfigFile = "../static/tests/pre-config0.json"
82 config, err := LoadPreConfig()
83diff --git a/gulpfile.js b/gulpfile.js
84index e33a752..904f3d1 100644
85--- a/gulpfile.js
86+++ b/gulpfile.js
87@@ -9,4 +9,8 @@ gulp.task('sass', function() {
88 }).on('error', sass.logError))
89 .pipe(uglifycss())
90 .pipe(gulp.dest('static/css'));
91-});
92\ No newline at end of file
93+});
94+
95+// Default: remember that these tasks get run asynchronously
96+gulp.task('default', ['sass']);
97+
98diff --git a/netman/dbus.go b/netman/dbus.go
99index 2db7107..8f36ac9 100644
100--- a/netman/dbus.go
101+++ b/netman/dbus.go
102@@ -20,16 +20,32 @@ package netman
103 import (
104 "errors"
105 "fmt"
106+ "io"
107 "log"
108 "os"
109 "strings"
110 "time"
111
112- "io"
113-
114 "github.com/godbus/dbus"
115 )
116
117+// Operations defines operations for this package client
118+type Operations interface {
119+ GetDevices() []string
120+ GetWifiDevices(devices []string) []string
121+ GetAccessPoints(devices []string, ap2device map[string]string) []string
122+ ConnectAp(ssid string, p string, ap2device map[string]string, ssid2ap map[string]string) error
123+ Ssids() ([]SSID, map[string]string, map[string]string)
124+ Connected(devices []string) bool
125+ ConnectedWifi(wifiDevices []string) bool
126+ DisconnectWifi(wifiDevices []string) int
127+ SetIfaceManaged(iface string, state bool, devices []string) string
128+ WifisManaged(wifiDevices []string) (map[string]string, error)
129+ Unmanage() error
130+ Manage() error
131+ ScanAndWriteSsidsToFile(filepath string) bool
132+}
133+
134 // Client type to support unit test mock and runtime execution
135 type Client struct {
136 dbusClient DbusClient
137diff --git a/package.json b/package.json
138index d3e2671..baf164b 100644
139--- a/package.json
140+++ b/package.json
141@@ -2,15 +2,15 @@
142 "name": "wifi-connect",
143 "version": "1.0.0",
144 "description": "Management UI to be able to switch a wireless card of a UC device into AP mode and use it to configure wireless",
145- "main": "index.js",
146 "scripts": {
147- "test": "echo \"Error: no test specified\" && exit 1"
148+ "test": "echo \"Error: no test specified\" && exit 1",
149+ "build": "gulp"
150 },
151 "repository": {
152 "type": "git",
153 "url": "git+ssh://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect"
154 },
155- "author": "",
156+ "author": "Canonical Ltd.",
157 "license": "GPL-3.0",
158 "bugs": {
159 "url": "https://bugs.launchpad.net/snappy-hwe-snaps"
160@@ -20,6 +20,6 @@
161 "gulp": "^3.9.1",
162 "gulp-sass": "^3.1.0",
163 "gulp-uglifycss": "^1.0.8",
164- "ubuntu-vanilla-theme": "0.0.35"
165+ "vanilla-framework": "^1.2.2"
166 }
167 }
168diff --git a/server/handlers.go b/server/handlers.go
169index f28c2d8..c14189f 100644
170--- a/server/handlers.go
171+++ b/server/handlers.go
172@@ -27,6 +27,8 @@ import (
173 "text/template"
174 "time"
175
176+ "strconv"
177+
178 "launchpad.net/wifi-connect/utils"
179 )
180
181@@ -34,19 +36,29 @@ const (
182 managementTemplatePath = "/templates/management.html"
183 connectingTemplatePath = "/templates/connecting.html"
184 operationalTemplatePath = "/templates/operational.html"
185+ firstConfigTemplatePath = "/templates/config.html"
186 )
187
188 // ResourcesPath absolute path to web static resources
189 var ResourcesPath = filepath.Join(os.Getenv("SNAP"), "static")
190
191+// first time management portal is accessed this file is created.
192+var firstConfigFlagFile = filepath.Join(os.Getenv("SNAP_COMMON"), ".first_config")
193+
194 var cw interface{}
195
196 // Data interface representing any data included in a template
197 type Data interface{}
198
199-// SsidsData dynamic data to fulfill the SSIDs page template
200-type SsidsData struct {
201- Ssids []string
202+// ManagementData dynamic data to fulfill the management page.
203+// It can contain SSIDs list or snap configuration
204+// NOTE: page is a workaround to render proper grid depending on its value (ssids or config).
205+// In case authentication mechanism is modified for not to be so intrusive, we could have
206+// different pages for this
207+type ManagementData struct {
208+ Ssids []string
209+ Config *utils.Config
210+ Page string
211 }
212
213 // ConnectingData dynamic data to fulfill the connect result page template
214@@ -61,22 +73,41 @@ func execTemplate(w http.ResponseWriter, templatePath string, data Data) {
215 templateAbsPath := filepath.Join(ResourcesPath, templatePath)
216 t, err := template.ParseFiles(templateAbsPath)
217 if err != nil {
218- log.Printf("Error loading the template at %v: %v", templatePath, err)
219- http.Error(w, err.Error(), http.StatusInternalServerError)
220+ msg := fmt.Sprintf("Error loading the template at %v: %v", templatePath, err)
221+ log.Println(msg)
222+ http.Error(w, msg, http.StatusInternalServerError)
223 return
224 }
225
226 err = t.Execute(w, data)
227 if err != nil {
228- log.Printf("Error executing the template at %v: %v", templatePath, err)
229- http.Error(w, err.Error(), http.StatusInternalServerError)
230+ msg := fmt.Sprintf("Error executing the template at %v : %v", templatePath, err)
231+ log.Print(msg)
232+ http.Error(w, msg, http.StatusInternalServerError)
233 return
234 }
235 }
236
237-// ManagementHandler lists the current available SSIDs
238+// ManagementHandler handles management portal
239 func ManagementHandler(w http.ResponseWriter, r *http.Request) {
240- // daemon stores current available ssids in a file
241+
242+ if utils.MustSetConfig() {
243+
244+ config, err := utils.ReadConfig()
245+ if err != nil {
246+ msg := fmt.Sprintf("Error reading configuration: %v", err)
247+ log.Println(msg)
248+ http.Error(w, msg, http.StatusInternalServerError)
249+ return
250+ }
251+
252+ if !config.Portal.NoResetCredentials {
253+ execTemplate(w, managementTemplatePath, ManagementData{Config: config, Page: "config"})
254+ return
255+ }
256+
257+ }
258+
259 ssids, err := utils.ReadSsidsFile()
260 if err != nil {
261 log.Printf("Error reading SSIDs file: %v", err)
262@@ -84,15 +115,58 @@ func ManagementHandler(w http.ResponseWriter, r *http.Request) {
263 return
264 }
265
266- data := SsidsData{Ssids: ssids}
267+ execTemplate(w, managementTemplatePath, ManagementData{Ssids: ssids, Page: "ssids"})
268+}
269+
270+// SaveConfigHandler saves config received as form post parameters
271+func SaveConfigHandler(w http.ResponseWriter, r *http.Request) {
272+ // read previous config
273+ config, err := utils.ReadConfig()
274+ if err != nil {
275+ msg := fmt.Sprintf("Error reading previous stored config: %v", err)
276+ log.Println(msg)
277+ http.Error(w, msg, http.StatusInternalServerError)
278+ return
279+ }
280+
281+ r.ParseForm()
282+
283+ config.Wifi.Ssid = utils.ParseFormParamSingleValue(r.Form, "Ssid")
284+ config.Wifi.Passphrase = utils.ParseFormParamSingleValue(r.Form, "Passphrase")
285+ config.Wifi.Interface = utils.ParseFormParamSingleValue(r.Form, "Interface")
286+ config.Wifi.CountryCode = utils.ParseFormParamSingleValue(r.Form, "CountryCode")
287+ config.Wifi.Channel, err = strconv.Atoi(utils.ParseFormParamSingleValue(r.Form, "Channel"))
288+ if err != nil {
289+ msg := fmt.Sprintf("Error parsing channel form value: %v", err)
290+ log.Println(msg)
291+ http.Error(w, msg, http.StatusInternalServerError)
292+ return
293+ }
294+ config.Wifi.OperationMode = utils.ParseFormParamSingleValue(r.Form, "OperationMode")
295+ config.Portal.Password = utils.ParseFormParamSingleValue(r.Form, "PortalPassword")
296+
297+ err = utils.WriteConfig(config)
298+ if err != nil {
299+ msg := fmt.Sprintf("Error saving config: %v", err)
300+ log.Println(msg)
301+ http.Error(w, msg, http.StatusInternalServerError)
302+ return
303+ }
304
305- // parse template
306- execTemplate(w, managementTemplatePath, data)
307+ //after saving config, redirect to management portal, showing available ssids
308+ ssids, err := utils.ReadSsidsFile()
309+ if err != nil {
310+ msg := fmt.Sprintf("== wifi-connect/handler: Error reading SSIDs file: %v\n", err)
311+ log.Println(msg)
312+ http.Error(w, msg, http.StatusInternalServerError)
313+ return
314+ }
315+
316+ execTemplate(w, managementTemplatePath, ManagementData{Ssids: ssids, Page: "ssids"})
317 }
318
319 // ConnectHandler reads form got ssid and password and tries to connect to that network
320 func ConnectHandler(w http.ResponseWriter, r *http.Request) {
321-
322 r.ParseForm()
323
324 pwd := ""
325@@ -103,7 +177,9 @@ func ConnectHandler(w http.ResponseWriter, r *http.Request) {
326
327 ssids := r.Form["ssid"]
328 if len(ssids) == 0 {
329- log.Print("SSID not available")
330+ msg := "SSID not available"
331+ log.Print(msg)
332+ http.Error(w, msg, http.StatusBadRequest)
333 return
334 }
335 ssid := ssids[0]
336@@ -116,7 +192,9 @@ func ConnectHandler(w http.ResponseWriter, r *http.Request) {
337
338 err := wifiapClient.Disable()
339 if err != nil {
340- log.Printf("Error disabling AP: %v", err)
341+ msg := fmt.Sprintf("Error disabling AP: %v", err)
342+ log.Print(msg)
343+ http.Error(w, msg, http.StatusInternalServerError)
344 return
345 }
346
347@@ -127,7 +205,9 @@ func ConnectHandler(w http.ResponseWriter, r *http.Request) {
348 err = netmanClient.ConnectAp(ssid, pwd, ap2device, ssid2ap)
349 //TODO signal user in portal on failure to connect
350 if err != nil {
351- log.Printf("Failed connecting to %v.", ssid)
352+ msg := fmt.Sprintf("Failed connecting to %v", ssid)
353+ log.Println(msg)
354+ http.Error(w, msg, http.StatusInternalServerError)
355 return
356 }
357
358diff --git a/server/handlers_test.go b/server/handlers_test.go
359index ad8eda7..336d242 100644
360--- a/server/handlers_test.go
361+++ b/server/handlers_test.go
362@@ -56,6 +56,10 @@ func (c *wifiapClientMock) SetPassphrase(string) error {
363 return nil
364 }
365
366+func (c *wifiapClientMock) Set(map[string]interface{}) error {
367+ return nil
368+}
369+
370 type netmanClientMock struct{}
371
372 func (c *netmanClientMock) GetDevices() []string {
373@@ -111,6 +115,26 @@ func (c *netmanClientMock) ScanAndWriteSsidsToFile(filepath string) bool {
374
375 func TestManagementHandler(t *testing.T) {
376
377+ // mock settings to not to ask for saving a first configuration
378+ utils.ReadConfig = func() (*utils.Config, error) {
379+ return &utils.Config{
380+ Wifi: &utils.WifiConfig{
381+ Ssid: "Ubuntu",
382+ Passphrase: "17Soj8/Sxh14lcpD",
383+ Interface: "wlp2s0",
384+ CountryCode: "0x31",
385+ Channel: 6,
386+ OperationMode: "g",
387+ },
388+ Portal: &utils.PortalConfig{
389+ Password: "the_password",
390+ NoResetCredentials: true,
391+ NoOperational: false,
392+ },
393+ }, nil
394+ }
395+ utils.MustSetConfig = func() bool { return false }
396+
397 ResourcesPath = "../static"
398 SsidsFile := "../static/tests/ssids"
399 utils.SetSsidsFile(SsidsFile)
400diff --git a/server/middleware.go b/server/middleware.go
401index a63c717..99bd6f9 100644
402--- a/server/middleware.go
403+++ b/server/middleware.go
404@@ -26,34 +26,8 @@ import (
405 "launchpad.net/wifi-connect/wifiap"
406 )
407
408-// Operations interface defining operations implemented by wifiap client
409-type wifiapOperations interface {
410- Show() (map[string]interface{}, error)
411- Enabled() (bool, error)
412- Enable() error
413- Disable() error
414- SetSsid(string) error
415- SetPassphrase(string) error
416-}
417-
418-type netmanOperations interface {
419- GetDevices() []string
420- GetWifiDevices(devices []string) []string
421- GetAccessPoints(devices []string, ap2device map[string]string) []string
422- ConnectAp(ssid string, p string, ap2device map[string]string, ssid2ap map[string]string) error
423- Ssids() ([]netman.SSID, map[string]string, map[string]string)
424- Connected(devices []string) bool
425- ConnectedWifi(wifiDevices []string) bool
426- DisconnectWifi(wifiDevices []string) int
427- SetIfaceManaged(iface string, state bool, devices []string) string
428- WifisManaged(wifiDevices []string) (map[string]string, error)
429- Unmanage() error
430- Manage() error
431- ScanAndWriteSsidsToFile(filepath string) bool
432-}
433-
434-var wifiapClient wifiapOperations
435-var netmanClient netmanOperations
436+var wifiapClient wifiap.Operations
437+var netmanClient netman.Operations
438
439 // Middleware to pre-process web service requests
440 func Middleware(inner http.Handler) http.Handler {
441diff --git a/server/router.go b/server/router.go
442index 47e63f3..20a3716 100644
443--- a/server/router.go
444+++ b/server/router.go
445@@ -28,7 +28,8 @@ func managementHandler() *mux.Router {
446 router := mux.NewRouter()
447
448 // Pages routes
449- router.HandleFunc("/", ManagementHandler).Methods("GET")
450+ router.Handle("/", Middleware(http.HandlerFunc(ManagementHandler))).Methods("GET")
451+ router.Handle("/config", Middleware(http.HandlerFunc(SaveConfigHandler))).Methods("POST")
452 router.Handle("/connect", Middleware(http.HandlerFunc(ConnectHandler))).Methods("POST")
453 router.HandleFunc("/hashit", HashItHandler).Methods("POST")
454 router.Handle("/refresh", Middleware(http.HandlerFunc(RefreshHandler))).Methods("GET")
455diff --git a/snap/scriptlets/country-codes b/snap/scriptlets/country-codes
456new file mode 100644
457index 0000000..d4e8a97
458--- /dev/null
459+++ b/snap/scriptlets/country-codes
460@@ -0,0 +1,243 @@
461+<html><head><title>ISO-3166-1 Country Names</title></head>
462+<body bgcolor=#ffffff>
463+<h2>ISO-3166-1 Country Names</h2>
464+<a href="/iso3166/">Reproduced with permission from ISO</a>
465+<p>
466+Follow links for ISO-3166-2 subdivision codes
467+<p>
468+<a href=iso.AD.html>AD</a> : ANDORRA<br>
469+<a href=iso.AE.html>AE</a> : UNITED ARAB EMIRATES<br>
470+<a href=iso.AF.html>AF</a> : AFGHANISTAN<br>
471+<a href=iso.AG.html>AG</a> : ANTIGUA AND BARBUDA<br>
472+<a href=iso.AI.html>AI</a> : ANGUILLA<br>
473+<a href=iso.AL.html>AL</a> : ALBANIA<br>
474+<a href=iso.AM.html>AM</a> : ARMENIA<br>
475+<a href=iso.AN.html>AN</a> : NETHERLANDS ANTILLES<br>
476+<a href=iso.AO.html>AO</a> : ANGOLA<br>
477+<a href=iso.AQ.html>AQ</a> : ANTARCTICA<br>
478+<a href=iso.AR.html>AR</a> : ARGENTINA<br>
479+<a href=iso.AS.html>AS</a> : AMERICAN SAMOA<br>
480+<a href=iso.AT.html>AT</a> : AUSTRIA<br>
481+<a href=iso.AU.html>AU</a> : AUSTRALIA<br>
482+<a href=iso.AW.html>AW</a> : ARUBA<br>
483+<a href=iso.AZ.html>AZ</a> : AZERBAIJAN<br>
484+<a href=iso.BA.html>BA</a> : BOSNIA AND HERZEGOVINA<br>
485+<a href=iso.BB.html>BB</a> : BARBADOS<br>
486+<a href=iso.BD.html>BD</a> : BANGLADESH<br>
487+<a href=iso.BE.html>BE</a> : BELGIUM<br>
488+<a href=iso.BF.html>BF</a> : BURKINA FASO<br>
489+<a href=iso.BG.html>BG</a> : BULGARIA<br>
490+<a href=iso.BH.html>BH</a> : BAHRAIN<br>
491+<a href=iso.BI.html>BI</a> : BURUNDI<br>
492+<a href=iso.BJ.html>BJ</a> : BENIN<br>
493+<a href=iso.BM.html>BM</a> : BERMUDA<br>
494+<a href=iso.BN.html>BN</a> : BRUNEI DARUSSALAM<br>
495+<a href=iso.BO.html>BO</a> : BOLIVIA<br>
496+<a href=iso.BR.html>BR</a> : BRAZIL<br>
497+<a href=iso.BS.html>BS</a> : BAHAMAS<br>
498+<a href=iso.BT.html>BT</a> : BHUTAN<br>
499+<a href=iso.BV.html>BV</a> : BOUVET ISLAND<br>
500+<a href=iso.BW.html>BW</a> : BOTSWANA<br>
501+<a href=iso.BY.html>BY</a> : BELARUS<br>
502+<a href=iso.BZ.html>BZ</a> : BELIZE<br>
503+<a href=iso.CA.html>CA</a> : CANADA<br>
504+<a href=iso.CC.html>CC</a> : COCOS (KEELING) ISLANDS<br>
505+<a href=iso.CD.html>CD</a> : CONGO, THE DEMOCRATIC REPUBLIC OF THE<br>
506+<a href=iso.CF.html>CF</a> : CENTRAL AFRICAN REPUBLIC<br>
507+<a href=iso.CG.html>CG</a> : CONGO<br>
508+<a href=iso.CH.html>CH</a> : SWITZERLAND<br>
509+<a href=iso.CI.html>CI</a> : C�TE D'IVOIRE<br>
510+<a href=iso.CK.html>CK</a> : COOK ISLANDS<br>
511+<a href=iso.CL.html>CL</a> : CHILE<br>
512+<a href=iso.CM.html>CM</a> : CAMEROON<br>
513+<a href=iso.CN.html>CN</a> : CHINA<br>
514+<a href=iso.CO.html>CO</a> : COLOMBIA<br>
515+<a href=iso.CR.html>CR</a> : COSTA RICA<br>
516+<a href=iso.CU.html>CU</a> : CUBA<br>
517+<a href=iso.CV.html>CV</a> : CAPE VERDE<br>
518+<a href=iso.CX.html>CX</a> : CHRISTMAS ISLAND<br>
519+<a href=iso.CY.html>CY</a> : CYPRUS<br>
520+<a href=iso.CZ.html>CZ</a> : CZECH REPUBLIC<br>
521+<a href=iso.DE.html>DE</a> : GERMANY<br>
522+<a href=iso.DJ.html>DJ</a> : DJIBOUTI<br>
523+<a href=iso.DK.html>DK</a> : DENMARK<br>
524+<a href=iso.DM.html>DM</a> : DOMINICA<br>
525+<a href=iso.DO.html>DO</a> : DOMINICAN REPUBLIC<br>
526+<a href=iso.DZ.html>DZ</a> : ALGERIA<br>
527+<a href=iso.EC.html>EC</a> : ECUADOR<br>
528+<a href=iso.EE.html>EE</a> : ESTONIA<br>
529+<a href=iso.EG.html>EG</a> : EGYPT<br>
530+<a href=iso.EH.html>EH</a> : WESTERN SARARA<br>
531+<a href=iso.ER.html>ER</a> : ERITREA<br>
532+<a href=iso.ES.html>ES</a> : SPAIN<br>
533+<a href=iso.ET.html>ET</a> : ETHIOPIA<br>
534+<a href=iso.FI.html>FI</a> : FINLAND<br>
535+<a href=iso.FJ.html>FJ</a> : FIJI<br>
536+<a href=iso.FK.html>FK</a> : FALKLAND ISLANDS (MALVINAS)<br>
537+<a href=iso.FM.html>FM</a> : MICRONESIA, FEDERATED STATES OF<br>
538+<a href=iso.FO.html>FO</a> : FAROE ISLANDS<br>
539+<a href=iso.FR.html>FR</a> : FRANCE<br>
540+<a href=iso.GA.html>GA</a> : GABON<br>
541+<a href=iso.GB.html>GB</a> : UNITED KINGDOM<br>
542+<a href=iso.GD.html>GD</a> : GRENADA<br>
543+<a href=iso.GE.html>GE</a> : GEORGIA<br>
544+<a href=iso.GF.html>GF</a> : FRENCH GUIANA<br>
545+<a href=iso.GH.html>GH</a> : GHANA<br>
546+<a href=iso.GI.html>GI</a> : GIBRALTAR<br>
547+<a href=iso.GL.html>GL</a> : GREENLAND<br>
548+<a href=iso.GM.html>GM</a> : GAMBIA<br>
549+<a href=iso.GN.html>GN</a> : GUINEA<br>
550+<a href=iso.GP.html>GP</a> : GUADELOUPE<br>
551+<a href=iso.GQ.html>GQ</a> : EQUATORIAL GUINEA<br>
552+<a href=iso.GR.html>GR</a> : GREECE<br>
553+<a href=iso.GS.html>GS</a> : SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS<br>
554+<a href=iso.GT.html>GT</a> : GUATEMALA<br>
555+<a href=iso.GU.html>GU</a> : GUAM<br>
556+<a href=iso.GW.html>GW</a> : GUINEA-BISSAU<br>
557+<a href=iso.GY.html>GY</a> : GUYANA<br>
558+<a href=iso.HK.html>HK</a> : HONG KONG<br>
559+<a href=iso.HM.html>HM</a> : HEARD ISLAND AND MCDONALD ISLANDS<br>
560+<a href=iso.HN.html>HN</a> : HONDURAS<br>
561+<a href=iso.HR.html>HR</a> : CROATIA<br>
562+<a href=iso.HT.html>HT</a> : HAITI<br>
563+<a href=iso.HU.html>HU</a> : HUNGARY<br>
564+<a href=iso.ID.html>ID</a> : INDONESIA<br>
565+<a href=iso.IE.html>IE</a> : IRELAND<br>
566+<a href=iso.IL.html>IL</a> : ISRAEL<br>
567+<a href=iso.IN.html>IN</a> : INDIA<br>
568+<a href=iso.IO.html>IO</a> : BRITISH INDIAN OCEAN TERRITORY<br>
569+<a href=iso.IQ.html>IQ</a> : IRAQ<br>
570+<a href=iso.IR.html>IR</a> : IRAN, ISLAMIC REPUBLIC OF<br>
571+<a href=iso.IS.html>IS</a> : ICELAND<br>
572+<a href=iso.IT.html>IT</a> : ITALY<br>
573+<a href=iso.JM.html>JM</a> : JAMAICA<br>
574+<a href=iso.JO.html>JO</a> : JORDAN<br>
575+<a href=iso.JP.html>JP</a> : JAPAN<br>
576+<a href=iso.KE.html>KE</a> : KENYA<br>
577+<a href=iso.KG.html>KG</a> : KYRGYZSTAN<br>
578+<a href=iso.KH.html>KH</a> : CAMBODIA<br>
579+<a href=iso.KI.html>KI</a> : KIRIBATI<br>
580+<a href=iso.KM.html>KM</a> : COMOROS<br>
581+<a href=iso.KN.html>KN</a> : SAINT KITTS AND NEVIS<br>
582+<a href=iso.KP.html>KP</a> : KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF<br>
583+<a href=iso.KR.html>KR</a> : KOREA, REPUBLIC OF<br>
584+<a href=iso.KW.html>KW</a> : KUWAIT<br>
585+<a href=iso.KY.html>KY</a> : CAYMAN ISLANDS<br>
586+<a href=iso.KZ.html>KZ</a> : KAZAKHSTAN<br>
587+<a href=iso.LA.html>LA</a> : LAO PEOPLE'S DEMOCRATIC REPUBLIC<br>
588+<a href=iso.LB.html>LB</a> : LEBANON<br>
589+<a href=iso.LC.html>LC</a> : SAINT LUCIA<br>
590+<a href=iso.LI.html>LI</a> : LIECHTENSTEIN<br>
591+<a href=iso.LK.html>LK</a> : SRI LANKA<br>
592+<a href=iso.LR.html>LR</a> : LIBERIA<br>
593+<a href=iso.LS.html>LS</a> : LESOTHO<br>
594+<a href=iso.LT.html>LT</a> : LITHUANIA<br>
595+<a href=iso.LU.html>LU</a> : LUXEMBOURG<br>
596+<a href=iso.LV.html>LV</a> : LATVIA<br>
597+<a href=iso.LY.html>LY</a> : LIBYAN ARAB JAMABIRIYA<br>
598+<a href=iso.MA.html>MA</a> : MOROCCO<br>
599+<a href=iso.MC.html>MC</a> : MONACO<br>
600+<a href=iso.MD.html>MD</a> : MOLDOVA, REPUBLIC OF<br>
601+<a href=iso.MG.html>MG</a> : MADAGASCAR<br>
602+<a href=iso.MH.html>MH</a> : MARSHALL ISLANDS<br>
603+<a href=iso.MK.html>MK</a> : MACEDONIA, THE FORMER YUGOSLAV REPU8LIC OF<br>
604+<a href=iso.ML.html>ML</a> : MALI<br>
605+<a href=iso.MM.html>MM</a> : MYANMAR<br>
606+<a href=iso.MN.html>MN</a> : MONGOLIA<br>
607+<a href=iso.MO.html>MO</a> : MACAU<br>
608+<a href=iso.MP.html>MP</a> : NORTHERN MARIANA ISLANDS<br>
609+<a href=iso.MQ.html>MQ</a> : MARTINIQUE<br>
610+<a href=iso.MR.html>MR</a> : MAURITANIA<br>
611+<a href=iso.MS.html>MS</a> : MONTSERRAT<br>
612+<a href=iso.MT.html>MT</a> : MALTA<br>
613+<a href=iso.MU.html>MU</a> : MAURITIUS<br>
614+<a href=iso.MV.html>MV</a> : MALDIVES<br>
615+<a href=iso.MW.html>MW</a> : MALAWI<br>
616+<a href=iso.MX.html>MX</a> : MEXICO<br>
617+<a href=iso.MY.html>MY</a> : MALAYSIA<br>
618+<a href=iso.MZ.html>MZ</a> : MOZAMBIQUE<br>
619+<a href=iso.NA.html>NA</a> : NAMIBIA<br>
620+<a href=iso.NC.html>NC</a> : NEW CALEDONIA<br>
621+<a href=iso.NE.html>NE</a> : NIGER<br>
622+<a href=iso.NF.html>NF</a> : NORFOLK ISLAND<br>
623+<a href=iso.NG.html>NG</a> : NIGERIA<br>
624+<a href=iso.NI.html>NI</a> : NICARAGUA<br>
625+<a href=iso.NL.html>NL</a> : NETHERLANDS<br>
626+<a href=iso.NO.html>NO</a> : NORWAY<br>
627+<a href=iso.NP.html>NP</a> : NEPAL<br>
628+<a href=iso.NU.html>NU</a> : NIUE<br>
629+<a href=iso.NZ.html>NZ</a> : NEW ZEALAND<br>
630+<a href=iso.OM.html>OM</a> : OMAN<br>
631+<a href=iso.PA.html>PA</a> : PANAMA<br>
632+<a href=iso.PE.html>PE</a> : PERU<br>
633+<a href=iso.PF.html>PF</a> : FRENCH POLYNESIA<br>
634+<a href=iso.PG.html>PG</a> : PAPUA NEW GUINEA<br>
635+<a href=iso.PH.html>PH</a> : PHILIPPINES<br>
636+<a href=iso.PK.html>PK</a> : PAKISTAN<br>
637+<a href=iso.PL.html>PL</a> : POLAND<br>
638+<a href=iso.PM.html>PM</a> : SAINT PIERRE AND MIQUELON<br>
639+<a href=iso.PN.html>PN</a> : PITCAIRN<br>
640+<a href=iso.PR.html>PR</a> : PUERTO RICO<br>
641+<a href=iso.PT.html>PT</a> : PORTUGAL<br>
642+<a href=iso.PW.html>PW</a> : PALAU<br>
643+<a href=iso.PY.html>PY</a> : PARAGUAY<br>
644+<a href=iso.QA.html>QA</a> : QATAR<br>
645+<a href=iso.RE.html>RE</a> : R�UNION<br>
646+<a href=iso.RO.html>RO</a> : ROMANIA<br>
647+<a href=iso.RU.html>RU</a> : RUSSIAN FEDERATION<br>
648+<a href=iso.RW.html>RW</a> : RWANDA<br>
649+<a href=iso.SA.html>SA</a> : SAUDI ARABIA<br>
650+<a href=iso.SB.html>SB</a> : SOLOMON ISLANDS<br>
651+<a href=iso.SC.html>SC</a> : SEYCHELLES<br>
652+<a href=iso.SD.html>SD</a> : SUDAN<br>
653+<a href=iso.SE.html>SE</a> : SWEDEN<br>
654+<a href=iso.SG.html>SG</a> : SINGAPORE<br>
655+<a href=iso.SH.html>SH</a> : SAINT HELENA<br>
656+<a href=iso.SI.html>SI</a> : SLOVENIA<br>
657+<a href=iso.SJ.html>SJ</a> : SVALBARD AND JAN MAYEN<br>
658+<a href=iso.SK.html>SK</a> : SLOVAKIA<br>
659+<a href=iso.SL.html>SL</a> : SIERRA LEONE<br>
660+<a href=iso.SM.html>SM</a> : SAN MARINO<br>
661+<a href=iso.SN.html>SN</a> : SENEGAL<br>
662+<a href=iso.SO.html>SO</a> : SOMALIA<br>
663+<a href=iso.SR.html>SR</a> : SURINAME<br>
664+<a href=iso.ST.html>ST</a> : SAO TOME AND PRINCIPE<br>
665+<a href=iso.SV.html>SV</a> : EL SALVADOR<br>
666+<a href=iso.SY.html>SY</a> : SYRIAN ARAB REPUBLIC<br>
667+<a href=iso.SZ.html>SZ</a> : SWAZILAND<br>
668+<a href=iso.TC.html>TC</a> : TURKS AND CAICOS ISLANDS<br>
669+<a href=iso.TD.html>TD</a> : CHAD<br>
670+<a href=iso.TF.html>TF</a> : FRENCH SOUTHERN TERRITORIES<br>
671+<a href=iso.TG.html>TG</a> : TOGO<br>
672+<a href=iso.TH.html>TH</a> : THAILAND<br>
673+<a href=iso.TJ.html>TJ</a> : TAJIKISTAN<br>
674+<a href=iso.TK.html>TK</a> : TOKELAU<br>
675+<a href=iso.TM.html>TM</a> : TURKMENISTAN<br>
676+<a href=iso.TN.html>TN</a> : TUNISIA<br>
677+<a href=iso.TO.html>TO</a> : TONGA<br>
678+<a href=iso.TP.html>TP</a> : EAST TIMOR<br>
679+<a href=iso.TR.html>TR</a> : TURKEY<br>
680+<a href=iso.TT.html>TT</a> : TRINIDAD AND TOBAGO<br>
681+<a href=iso.TV.html>TV</a> : TUVALU<br>
682+<a href=iso.TW.html>TW</a> : TAIWAN, PROVINCE OF CHINA<br>
683+<a href=iso.TZ.html>TZ</a> : TANZANIA, UNITED REPUBLIC OF<br>
684+<a href=iso.UA.html>UA</a> : UKRAINE<br>
685+<a href=iso.UG.html>UG</a> : UGANDA<br>
686+<a href=iso.UM.html>UM</a> : UNITED STATES MINOR OUTLYING ISLANDS<br>
687+<a href=iso.US.html>US</a> : UNITED STATES<br>
688+<a href=iso.UY.html>UY</a> : URUGUAY<br>
689+<a href=iso.UZ.html>UZ</a> : UZBEKISTAN<br>
690+<a href=iso.VE.html>VE</a> : VENEZUELA<br>
691+<a href=iso.VG.html>VG</a> : VIRGIN ISLANDS, BRITISH<br>
692+<a href=iso.VI.html>VI</a> : VIRGIN ISLANDS, U.S.<br>
693+<a href=iso.VN.html>VN</a> : VIET NAM<br>
694+<a href=iso.VU.html>VU</a> : VANUATU<br>
695+<a href=iso.WF.html>WF</a> : WALLIS AND FUTUNA<br>
696+<a href=iso.WS.html>WS</a> : SAMOA<br>
697+<a href=iso.YE.html>YE</a> : YEMEN<br>
698+<a href=iso.YT.html>YT</a> : MAYOTTE<br>
699+<a href=iso.YU.html>YU</a> : YUGOSLAVIA<br>
700+<a href=iso.ZA.html>ZA</a> : SOUTH AFRICA<br>
701+<a href=iso.ZM.html>ZM</a> : ZAMBIA<br>
702+<a href=iso.ZW.html>ZW</a> : ZIMBABWE<br>
703+</body></html>
704diff --git a/snap/scriptlets/fetch_country_codes.sh b/snap/scriptlets/fetch_country_codes.sh
705new file mode 100755
706index 0000000..8dae525
707--- /dev/null
708+++ b/snap/scriptlets/fetch_country_codes.sh
709@@ -0,0 +1,10 @@
710+#!/bin/sh
711+# The purpose of this script is taking ISO-3166 country codes and
712+# store them locally to be used in compilation time.
713+# This operation must be executed by hand when wanted to update
714+# current ones shown in config page
715+set -e
716+
717+cd "$(dirname "$0")"
718+
719+curl -X GET http://geotags.com/iso3166/countries.html > country-codes
720\ No newline at end of file
721diff --git a/snap/scriptlets/fill_country_codes.sh b/snap/scriptlets/fill_country_codes.sh
722new file mode 100755
723index 0000000..451c61b
724--- /dev/null
725+++ b/snap/scriptlets/fill_country_codes.sh
726@@ -0,0 +1,39 @@
727+#!/bin/sh
728+# The purpose of this script is taking ISO-3166 country codes from country_code file
729+# so that they can be updated in management portal when snap is built
730+# Process is easy, get it from remote path, format using sed, and replace
731+# html page were they will be used
732+
733+set -e
734+
735+COUNTRY_CODES=../../../snap/scriptlets/country-codes
736+
737+if [ ! -e $COUNTRY_CODES ]; then
738+ echo "======================================================="
739+ echo "Could not find country_codes needed file for compiling."
740+ echo "Please, before building snap for the first time execute by hand:"
741+ echo ""
742+ echo "snap/scriptlets/fetch_country_codes.sh"
743+ echo ""
744+ echo "======================================================="
745+ exit 1
746+fi
747+
748+# think that this is a scriptlet, executed in parts/<the_part>/build folder
749+cp $COUNTRY_CODES .
750+
751+# remove non processable lines
752+sed -i '/^<a href=iso/!d' country-codes
753+
754+# process lines to change its format to be html select options
755+sed -i '/<a href=iso/ s/<a href=iso.*html>/<option value="/
756+s/<\/a>\ :\ /">/
757+s/<br>/<\/option>/' country-codes
758+
759+# add world wide default option at beginning
760+sed -i '1s;^;<option value="XX">\-WORLD WIDE\-<\/option>\n;' country-codes
761+
762+# replace mark in management.html template with real country-codes
763+sed -e '/\[COUNTRY_CODE_OPTIONS\]/ {' -e 'r country-codes' -e 'd' -e '}' -i static/templates/management.html
764+
765+echo "country codes filled ok"
766diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
767index 12f6cbc..3a7cf44 100644
768--- a/snap/snapcraft.yaml
769+++ b/snap/snapcraft.yaml
770@@ -56,5 +56,6 @@ parts:
771 assets:
772 plugin: dump
773 source: .
774+ prepare: ../../../snap/scriptlets/fill_country_codes.sh
775 stage:
776 - static
777diff --git a/static/css/application.css b/static/css/application.css
778index ab1e598..b0edc63 100644
779--- a/static/css/application.css
780+++ b/static/css/application.css
781@@ -1 +1 @@
782-@charset "UTF-8";*{font-smoothing:subpixel-antialiased;box-sizing:border-box}button,input[type='button'],input[type='reset'],input[type='submit'],.button--primary,.button--secondary{font-smoothing:subpixel-antialiased;font-size:1em;display:inline-block;width:100%;margin:0;padding:11px 24px;text-align:center;text-decoration:none;border:0;border-radius:2px;outline:0;font-weight:300;cursor:pointer}button:hover,input[type='button']:hover,input[type='reset']:hover,input[type='submit']:hover,.button--primary:hover,.button--secondary:hover{text-decoration:none}@media only screen and (min-width:768px){button,input[type='button'],input[type='reset'],input[type='submit'],.button--primary,.button--secondary{width:auto}}.clearfix{display:block}.clearfix:after{clear:both;content:'\0020';display:block;height:0;overflow:hidden;visibility:hidden}input[type='text'],input[type='number'],input[type='search'],input[type='password'],input[type='email'],input[type='url'],input[type='tel'],textarea{-webkit-appearance:textfield;border-radius:2px;border:1px solid #cdcdcd;box-shadow:inset 0 1px 2px rgba(0,0,0,0.12);font-size:16px;margin:0;outline:0;padding:.6956522em .869565em;vertical-align:baseline;font-weight:300}input[type='text']:active,input[type='number']:active,input[type='search']:active,input[type='password']:active,input[type='email']:active,input[type='url']:active,input[type='tel']:active,textarea:active,input[type='text']:focus,input[type='number']:focus,input[type='search']:focus,input[type='password']:focus,input[type='email']:focus,input[type='url']:focus,input[type='tel']:focus,textarea:focus{border-color:#888;outline:0}input[type='text']::placeholder,input[type='number']::placeholder,input[type='search']::placeholder,input[type='password']::placeholder,input[type='email']::placeholder,input[type='url']::placeholder,input[type='tel']::placeholder,textarea::placeholder{color:contrast-friendly-search-color(#e95420)}input[disabled="disabled"][type='text'],input[disabled="disabled"][type='number'],input[disabled="disabled"][type='search'],input[disabled="disabled"][type='password'],input[disabled="disabled"][type='email'],input[disabled="disabled"][type='url'],input[disabled="disabled"][type='tel'],textarea[disabled="disabled"]{opacity:.5}input[type='checkbox'],input[type='radio']{border-radius:2px;border:1px solid #cdcdcd;box-shadow:inset 0 1px 2px rgba(0,0,0,0.12);height:14px;padding:0;vertical-align:middle;width:14px}.inner-wrapper{margin:0 auto;max-width:984px}[class*='-col'].last-col,[class*='-col'] [class*='-col'].last-col{margin-right:0}.one-col,.two-col,.three-col,.four-col,.five-col,.six-col,.seven-col,.eight-col,.nine-col,.ten-col,.eleven-col,.twelve-col{clear:none;margin-bottom:20px;margin-right:0;padding:0;display:inline-block;position:relative;width:100%}@media only screen and (min-width:768px){.one-col,.two-col,.three-col,.four-col,.five-col,.six-col,.seven-col,.eight-col,.nine-col,.ten-col,.eleven-col,.twelve-col{float:left}}.list-ticks--compact,.list-ticks--canonical-compact,.list-ticks,.list-ticks--canonical,.list{list-style:none;margin-left:0}.list-ticks--compact li,.list-ticks--canonical-compact li,.list-ticks li,.list-ticks--canonical li,.list li{border-bottom:1px dotted #888;margin-bottom:0;padding:10px 0}.list-ticks--compact li:last-of-type,.list-ticks--canonical-compact li:last-of-type,.list-ticks li:last-of-type,.list-ticks--canonical li:last-of-type,.list li:last-of-type,.list-ticks--compact .last-item,.list-ticks--canonical-compact .last-item,.list-ticks .last-item,.list-ticks--canonical .last-item,.list .last-item{border:0;margin-bottom:0;padding-bottom:0}.list-ticks--compact li,.list-ticks--canonical-compact li,.list-ticks li,.list-ticks--canonical li{background-repeat:no-repeat;background-position:0 1em;padding-left:25px}.box,.box-grey{background:#fff;border:1px solid #cdcdcd;padding:1.25em 20px}@media only screen and (min-width:768px){.equal-height,.equal-height--vertical-divider{display:flex;flex-wrap:wrap;flex-direction:row;width:100%}.equal-height .equal-height__item,.equal-height--vertical-divider .equal-height__item{display:flex;flex:auto;flex-direction:column}}.arrow-up,.arrow-down,.arrow-right,.arrow-left{background-position:0 0;background-repeat:no-repeat;height:11px;position:absolute;width:18px}.box,.box-grey{border-radius:4px;padding:1.333em 20px}.list-ticks--compact li,.list-ticks--canonical-compact li{border:0;padding:10px 0 0 25px}html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,ol,ul,li,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;margin:0;padding:0;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,nav,section{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none}[hidden]{display:none}html{font-size:100%;overflow-y:scroll;text-size-adjust:100%}blockquote,q{quotes:none}legend{border:0}figure{margin:0}abbr,acronym{cursor:help}.link-arrow:after{content:'\0000a0›'}nav ul li h2 a:after{content:'\0000a0›'}nav ul li a:after,.carousel ul li a:after,ul li p a:after{content:''}svg:not(:root){overflow:hidden}img{border:0;height:auto;max-width:100%}img .left{margin-right:20px}img .right{margin-left:20px}.middle img{vertical-align:middle;margin-top:4em}ins{background:#fffbeb;text-decoration:none}.left{float:left}.right{float:right}.no-border{border:0}.link-top{font-size:.875em;clear:both;margin-bottom:40px;margin-top:-40px}.link-top a{background:#fff;margin-right:10px;margin-top:-35px;padding:5px;float:right}.caps{text-transform:uppercase}.touch-border,.touch-bottom{float:left}@media only screen and (min-width:768px){.touch-border,.touch-bottom{margin-bottom:-20px}}@media only screen and (min-width:984px){.touch-border,.touch-bottom{margin-bottom:-40px}}.accessibility-aid,.off-left{position:absolute;left:-999em}.external{background-size:.7em .7em;padding-right:.9em;background-image:url("https://assets.ubuntu.com/v1/f24a8aa0-external-link-orange.svg");background-position:100% top;background-repeat:no-repeat}.no-svg .external{background-image:url("https://assets.ubuntu.com/v1/f24a8aa0-external-link-orange.svg")}.text-center,.align-center{text-align:center}.no-margin-bottom{margin-bottom:0}.no-padding-bottom{padding-bottom:0}.pull-bottom-right{float:right;margin-right:-10px}@media only screen and (min-width:768px){.pull-bottom-right{margin-right:-40px;margin-bottom:-40px}}@media only screen and (min-width:984px){.pull-bottom-right{margin-right:-40px;margin-bottom:-60px}}.pull-bottom-left{float:left;margin-left:-10px}@media only screen and (min-width:768px){.pull-bottom-left{margin-left:-40px;margin-bottom:-40px}}@media only screen and (min-width:984px){.pull-bottom-left{margin-left:-40px;margin-bottom:-60px}}@media only screen and (max-width:767px){.priority-0,.not-for-small{display:none}}@media only screen and (min-width:768px){.for-mobile,.for-small{display:none}}.video-container{position:relative;padding-bottom:56.25%;padding-top:30px;height:0;overflow:hidden}.video-container iframe{position:absolute;top:0;left:0;width:100%;height:100%}.video-container+.video-title{margin-top:20px}.pull-right{float:right;margin-right:-40px}@media only screen and (max-width:768px){.pull-right{margin-bottom:20px}}.pull-left{margin-left:-40px}.for-tablet,.for-medium{display:none}@media only screen and (min-width:768px){.for-tablet,.for-medium{display:block}}@media only screen and (min-width:768px) and (max-width:984px){.med-six-col .three-col{width:48.93617%}.med-six-col .three-col:nth-of-type(2n){margin-right:0}}blockquote{margin:0}blockquote>p{font-size:1em;margin:0 0 .4em}blockquote small{font-size:.8125em;line-height:1.4}.pull-quote{text-indent:0}.pull-quote p{font-size:1.5em;line-height:1.3;margin-left:.4em;padding-left:10px;padding-right:10px;text-indent:-.4em}.pull-quote p span{color:#e95420;font-weight:bold;left:-5px;line-height:0;position:relative}.pull-quote p span:last-of-type{left:5px}.pull-quote p cite{display:block;font-size:.75em;font-weight:300;margin:10px 0 0;text-indent:0}@media only screen and (min-width:768px){.pull-quote p{padding-left:0;padding-right:0;text-indent:-.7em}.pull-quote p span{top:5px;font-size:1.391304348em}.pull-quote p cite{margin-left:0;text-indent:0}}@media only screen and (min-width:984px){.pull-quote p span{top:10px}}.row-quote{border-radius:0}.row-quote blockquote{border-radius:4px;margin:0;padding:0}.row-quote blockquote p{color:#333;line-height:1.3;margin-bottom:.75em;padding-left:10px;padding-right:10px;text-indent:0}.row-quote blockquote span{color:#e95420;font-weight:bold;left:-5px;line-height:0;position:relative}.row-quote blockquote span+span{left:5px}.row-quote blockquote cite{color:#333;font-size:.75em;font-style:normal;margin-bottom:0;text-indent:0}@media only screen and (min-width:768px){.row-quote{text-indent:-.4em}.row-quote blockquote{text-indent:-7px}.row-quote blockquote p{font-size:1.5em;padding-left:0;padding-right:0;text-indent:-.4em}.row-quote blockquote span{top:5px;font-size:1.391304348em}.row-quote blockquote cite{margin-left:0;text-indent:0}}@media only screen and (min-width:984px){.row-quote blockquote{padding:0 80px 20px;text-indent:-10px}.row-quote blockquote p span{top:10px}}button,input[type='button'],input[type='reset'],input[type='submit'],.button--primary{color:#fff;background-color:#e95420}button:hover,input[type='button']:hover,input[type='reset']:hover,input[type='submit']:hover,.button--primary:hover{background-color:#d44615}button .external,input[type='button'] .external,input[type='reset'] .external,input[type='submit'] .external,.button--primary .external{background-image:url("https://assets.ubuntu.com/v1/484aba04-external-link-white.svg")}.button--secondary{color:#e95420;background-color:#fff;border:1px solid #cdcdcd}.button--secondary:hover{background-color:#efefef}@media only screen and (min-width:860px){.nav-secondary{border:1px solid #d2d2d2;border-top:0}}.nav-secondary .breadcrumb{margin-bottom:0;margin-left:0;padding-top:2px;padding-bottom:3px}@media only screen and (min-width:860px){.nav-secondary .breadcrumb{float:left;margin-left:4px}}.nav-secondary .breadcrumb li{color:#fff;display:inline-block;margin:0;width:100%}@media only screen and (min-width:860px){.nav-secondary .breadcrumb li{display:inline-block;width:auto}}.nav-secondary .breadcrumb li:first-of-type{border-bottom:1px solid #d2d2d2}@media only screen and (min-width:860px){.nav-secondary .breadcrumb li:first-of-type{border-bottom:0}}.nav-secondary .breadcrumb li a{color:#888;font-size:1em;display:inline-block;width:100%;margin-right:0;padding:8px 10px;text-decoration:none}@media only screen and (min-width:860px){.nav-secondary .breadcrumb li a{font-size:.875em}}@media only screen and (min-width:860px){.nav-secondary .breadcrumb li .breadcrumb-link,.nav-secondary .breadcrumb li .breadcrumb-link--second-level,.nav-secondary .breadcrumb li .active{display:inline-block;float:none;width:auto}}.nav-secondary .breadcrumb li .breadcrumb-link:after,.nav-secondary .breadcrumb li .breadcrumb-link--second-level:after{content:'\203A';display:inline-block;height:5px;margin-left:8px;position:absolute;width:5px}.nav-secondary .second-level-nav,.nav-secondary .third-level-nav{display:none;overflow:hidden;width:100%;margin:0;padding-top:10px;padding-bottom:10px;border-bottom:1px solid #d2d2d2}@media only screen and (min-width:860px){.nav-secondary .second-level-nav,.nav-secondary .third-level-nav{display:inline;float:none;margin-bottom:0;padding:0;width:auto;border-bottom:0}}.nav-secondary .second-level-nav li,.nav-secondary .third-level-nav li{margin:0}@media only screen and (min-width:860px){.nav-secondary .second-level-nav li,.nav-secondary .third-level-nav li{float:none;width:auto}}.nav-secondary .second-level-nav li a,.nav-secondary .third-level-nav li a{font-size:.875em;display:block;width:100%;height:100%;padding:10px;color:#333}@media only screen and (max-width:860px){.nav-secondary .second-level-nav li a.active,.nav-secondary .third-level-nav li a.active{font-weight:500}}@media only screen and (min-width:860px){.nav-secondary .second-level-nav li a,.nav-secondary .third-level-nav li a{padding:8px 10px 0}.nav-secondary .second-level-nav li a.active,.nav-secondary .third-level-nav li a.active{color:#e95420}}@media only screen and (min-width:860px){.nav-secondary .second-level-nav li{width:auto}.nav-secondary .second-level-nav li .active,.nav-secondary .second-level-nav li .breadcrumb-link--second-level{color:#888}.nav-secondary .second-level-nav li .third-level-nav li a{color:#333}.nav-secondary .second-level-nav li .third-level-nav li a:hover{color:#e95420}.nav-secondary .second-level-nav li .third-level-nav li .active{color:#e95420;float:none}.nav-secondary .second-level-nav li .third-level-nav li .active:after{content:''}}.third-level-nav li a:hover{color:#e95420}.third-level-nav li .active{color:#e95420;float:none}@media only screen and (max-width:859px){#breadcrumbs:hover .second-level-nav,#breadcrumbs:hover .third-level-nav,#breadcrumbs:hover .active{border:0;display:block;overflow:visible;float:left;width:100%;position:relative;clear:both}#breadcrumbs:hover li{border:0;float:left}#breadcrumbs:hover .second-level-nav{border-top:1px solid #d2d2d2}#breadcrumbs:hover .second-level-nav li{float:left}#breadcrumbs:hover .third-level-nav{border-bottom:1px solid #d2d2d2;padding-top:0}#breadcrumbs:hover .third-level-nav li{width:50%;display:block;padding-left:10px}}label{cursor:pointer;display:block;margin-bottom:4px}label.has-error{color:#df382c}label.has-warning{color:#eca918}label.has-success{color:#38b44a}label.has-information{color:#19b6ee}input[type='reset']{display:none}input[type='search']{-webkit-appearance:none;border-radius:0}input[type='checkbox']+label,input[type='radio']+label{display:inline;margin-left:5px;vertical-align:middle;width:auto}input.has-error{border:1px solid #df382c}input.has-warning{border:1px solid #eca918}input.has-success{border:1px solid #38b44a}input.has-information{border:1px solid #19b6ee}form ul{margin-left:0;list-style:none}textarea{overflow:auto;vertical-align:top}textarea[readonly='readonly']{color:#888;cursor:default}textarea[readonly='readonly']:active,textarea[readonly='readonly']:focus{border-color:#888;outline:0}fieldset{background-color:#f7f7f7;background-position:-15px -15px;background-repeat:no-repeat;border-radius:4px;border:0;margin-bottom:8px;padding:15px 20px}@media only screen and (min-width:984px){fieldset{padding:15px 20px}}fieldset h3{border-bottom:1px dotted #cdcdcd;margin-bottom:9px;padding-bottom:10px}form input,form select,form textarea{width:100%}@media only screen and (min-width:768px){.one-col{float:left;width:6.30531%;margin-right:2.21239%}.one-col .one-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.two-col{float:left;width:14.82301%;margin-right:2.21239%}.two-col .one-col{float:left;width:43.50649%;margin-right:12.98701%}.two-col .two-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.three-col{float:left;width:23.34071%;margin-right:2.21239%}.three-col .one-col{float:left;width:27.56133%;margin-right:8.65801%}.three-col .two-col{float:left;width:63.78066%;margin-right:8.65801%}.three-col .three-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.four-col{float:left;width:31.85841%;margin-right:2.21239%}.four-col .one-col{float:left;width:20.12987%;margin-right:6.49351%}.four-col .two-col{float:left;width:46.75325%;margin-right:6.49351%}.four-col .three-col{float:left;width:73.37662%;margin-right:6.49351%}.four-col .four-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.five-col{float:left;width:40.37611%;margin-right:2.21239%}.five-col .one-col{float:left;width:15.84416%;margin-right:5.19481%}.five-col .two-col{float:left;width:36.88312%;margin-right:5.19481%}.five-col .three-col{float:left;width:57.92208%;margin-right:5.19481%}.five-col .four-col{float:left;width:78.96104%;margin-right:5.19481%}.five-col .five-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.six-col{float:left;width:48.89381%;margin-right:2.21239%}.six-col .one-col{float:left;width:13.05916%;margin-right:4.329%}.six-col .two-col{float:left;width:30.44733%;margin-right:4.329%}.six-col .three-col{float:left;width:47.8355%;margin-right:4.329%}.six-col .four-col{float:left;width:65.22367%;margin-right:4.329%}.six-col .five-col{float:left;width:82.61183%;margin-right:4.329%}.six-col .six-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.seven-col{float:left;width:57.4115%;margin-right:2.21239%}.seven-col .one-col{float:left;width:11.10522%;margin-right:3.71058%}.seven-col .two-col{float:left;width:25.92102%;margin-right:3.71058%}.seven-col .three-col{float:left;width:40.73681%;margin-right:3.71058%}.seven-col .four-col{float:left;width:55.55261%;margin-right:3.71058%}.seven-col .five-col{float:left;width:70.36841%;margin-right:3.71058%}.seven-col .six-col{float:left;width:85.1842%;margin-right:3.71058%}.seven-col .seven-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.eight-col{float:left;width:65.9292%;margin-right:2.21239%}.eight-col .one-col{float:left;width:9.65909%;margin-right:3.24675%}.eight-col .two-col{float:left;width:22.56494%;margin-right:3.24675%}.eight-col .three-col{float:left;width:35.47078%;margin-right:3.24675%}.eight-col .four-col{float:left;width:48.37662%;margin-right:3.24675%}.eight-col .five-col{float:left;width:61.28247%;margin-right:3.24675%}.eight-col .six-col{float:left;width:74.18831%;margin-right:3.24675%}.eight-col .seven-col{float:left;width:87.09416%;margin-right:3.24675%}.eight-col .eight-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.nine-col{float:left;width:74.4469%;margin-right:2.21239%}.nine-col .one-col{float:left;width:8.54578%;margin-right:2.886%}.nine-col .two-col{float:left;width:19.97755%;margin-right:2.886%}.nine-col .three-col{float:left;width:31.40933%;margin-right:2.886%}.nine-col .four-col{float:left;width:42.84111%;margin-right:2.886%}.nine-col .five-col{float:left;width:54.27289%;margin-right:2.886%}.nine-col .six-col{float:left;width:65.70467%;margin-right:2.886%}.nine-col .seven-col{float:left;width:77.13644%;margin-right:2.886%}.nine-col .eight-col{float:left;width:88.56822%;margin-right:2.886%}.nine-col .nine-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.ten-col{float:left;width:82.9646%;margin-right:2.21239%}.ten-col .one-col{float:left;width:7.66234%;margin-right:2.5974%}.ten-col .two-col{float:left;width:17.92208%;margin-right:2.5974%}.ten-col .three-col{float:left;width:28.18182%;margin-right:2.5974%}.ten-col .four-col{float:left;width:38.44156%;margin-right:2.5974%}.ten-col .five-col{float:left;width:48.7013%;margin-right:2.5974%}.ten-col .six-col{float:left;width:58.96104%;margin-right:2.5974%}.ten-col .seven-col{float:left;width:69.22078%;margin-right:2.5974%}.ten-col .eight-col{float:left;width:79.48052%;margin-right:2.5974%}.ten-col .nine-col{float:left;width:89.74026%;margin-right:2.5974%}.ten-col .ten-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.eleven-col{float:left;width:91.4823%;margin-right:2.21239%}.eleven-col .one-col{float:left;width:6.9443%;margin-right:2.36128%}.eleven-col .two-col{float:left;width:16.24987%;margin-right:2.36128%}.eleven-col .three-col{float:left;width:25.55544%;margin-right:2.36128%}.eleven-col .four-col{float:left;width:34.86101%;margin-right:2.36128%}.eleven-col .five-col{float:left;width:44.16658%;margin-right:2.36128%}.eleven-col .six-col{float:left;width:53.47215%;margin-right:2.36128%}.eleven-col .seven-col{float:left;width:62.77772%;margin-right:2.36128%}.eleven-col .eight-col{float:left;width:72.08329%;margin-right:2.36128%}.eleven-col .nine-col{float:left;width:81.38886%;margin-right:2.36128%}.eleven-col .ten-col{float:left;width:90.69443%;margin-right:2.36128%}.eleven-col .eleven-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.prepend-one{margin-left:8.5177%}.prepend-two{margin-left:17.0354%}.prepend-three{margin-left:25.5531%}.prepend-four{margin-left:34.0708%}.prepend-five{margin-left:42.5885%}.prepend-six{margin-left:51.10619%}.prepend-seven{margin-left:59.62389%}.prepend-eight{margin-left:68.14159%}.prepend-nine{margin-left:76.65929%}.prepend-ten{margin-left:85.17699%}.prepend-eleven{margin-left:93.69469%}}@media only screen and (min-width:768px){.append-one{margin-right:8.5177%}.append-two{margin-right:17.0354%}.append-three{margin-right:25.5531%}.append-four{margin-right:34.0708%}.append-five{margin-right:42.5885%}.append-six{margin-right:51.10619%}.append-seven{margin-right:59.62389%}.append-eight{margin-right:68.14159%}.append-nine{margin-right:76.65929%}.append-ten{margin-right:85.17699%}.append-eleven{margin-right:93.69469%}}@media only screen and (min-width:768px){.push-one{float:left;position:relative;margin:0 -8.5177% 0 8.5177%}.push-two{float:left;position:relative;margin:0 -19.24779% 0 19.24779%}.push-three{float:left;position:relative;margin:0 -27.76549% 0 27.76549%}.push-four{float:left;position:relative;margin:0 -36.28319% 0 36.28319%}.push-five{float:left;position:relative;margin:0 -44.80088% 0 44.80088%}.push-six{float:left;position:relative;margin:0 -53.31858% 0 53.31858%}.push-seven{float:left;position:relative;margin:0 -61.83628% 0 61.83628%}.push-eight{float:left;position:relative;margin:0 -70.35398% 0 70.35398%}.push-nine{float:left;position:relative;margin:0 -78.87168% 0 78.87168%}.push-ten{float:left;position:relative;margin:0 -87.38938% 0 87.38938%}.push-eleven{float:left;position:relative;margin:0 -95.90708% 0 95.90708%}}@media only screen and (min-width:768px){.pull-one{float:left;position:relative;margin-left:-6.30531%}.pull-two{float:left;position:relative;margin-left:17.0354%}.pull-three{float:left;position:relative;margin-left:25.5531%}.pull-four{float:left;position:relative;margin-left:34.0708%}.pull-five{float:left;position:relative;margin-left:42.5885%}.pull-six{float:left;position:relative;margin-left:51.10619%}.pull-seven{float:left;position:relative;margin-left:59.62389%}.pull-eight{float:left;position:relative;margin-left:68.14159%}.pull-nine{float:left;position:relative;margin-left:76.65929%}.pull-ten{float:left;position:relative;margin-left:85.17699%}.pull-eleven{float:left;position:relative;margin-left:93.69469%}}.banner{background:#e95420;border-bottom:0;border-top:0;box-shadow:inset 0 -1px 0 #ccc;display:block;min-width:100%;position:relative;width:auto;z-index:2}@media only screen and (min-width:860px){.banner{border-bottom:1px solid #ccc;box-shadow:none}}.banner .logo{position:relative;display:table;float:left;overflow:hidden;height:48px;margin:0 10px;border-left:0}@media only screen and (min-width:860px){.banner .logo{margin:0 15px}}.banner .logo a{display:table-cell;height:100%;vertical-align:middle;color:#fff}.banner .logo a span{display:inline-block}.banner h2{font-size:1.563em;position:relative;top:14px;left:4px;display:block;margin-bottom:0;text-transform:lowercase}.banner h2 a,.banner h2 a:link,.banner h2 a:visited{float:left;text-decoration:none;color:#fff}.banner .nav-primary{overflow:hidden;margin:0 auto;border:0}@media only screen and (min-width:860px){.banner .nav-primary{max-width:984px}}.banner .nav-primary span{display:none}.banner .nav-primary ul{display:none;float:left;width:100%;margin:0;padding:0;border-top:1px solid #ccc;box-shadow:inset 0 -1px 0 #ccc;background-color:#ebebeb}@media only screen and (min-width:860px){.banner .nav-primary ul{position:static;display:block;width:auto;border-top:0;background-color:transparent;box-shadow:none}}.banner .nav-primary ul li{float:left;width:50%;margin:0;border-right:1px solid #ccc;border-bottom:1px solid #ccc;background:transparent}@media only screen and (min-width:860px){.banner .nav-primary ul li{width:auto;border-right:0;border-bottom:0;border-left:1px solid #cc4414}.banner .nav-primary ul li:last-child{border-right:1px solid #cc4414}.banner .nav-primary ul li:last-child a{border-right:0}}@media only screen and (max-width:860px){.banner .nav-primary ul li:nth-child(2n+2){border-right:0}}.banner .nav-primary ul li a,.banner .nav-primary ul li a:link,.banner .nav-primary ul li a:visited{font-size:1em;font-weight:300;position:relative;display:block;margin-bottom:0;padding:8px 10px;text-align:left;text-decoration:none;background-color:#ebebeb;font-smoothing:subpixel-antialiased;color:#333}@media only screen and (min-width:860px){.banner .nav-primary ul li a,.banner .nav-primary ul li a:link,.banner .nav-primary ul li a:visited{font-size:.875em;padding:14px 14px 13px;text-align:center;border-left:1px solid #ee7f58;background-color:transparent;color:#fff}}.banner .nav-primary ul li a:hover,.banner .nav-primary ul li a:link:hover,.banner .nav-primary ul li a:visited:hover{background:#f2f2f2}@media only screen and (min-width:860px){.banner .nav-primary ul li a:hover,.banner .nav-primary ul li a:link:hover,.banner .nav-primary ul li a:visited:hover{background-color:#ed7045}}.banner .nav-primary ul li a.active,.banner .nav-primary ul li a:link.active,.banner .nav-primary ul li a:visited.active{background-color:#ddd}@media only screen and (min-width:860px){.banner .nav-primary ul li a.active,.banner .nav-primary ul li a:link.active,.banner .nav-primary ul li a:visited.active{background-color:#cc4414;border-left:0}}.banner .nav-toggle{position:absolute;top:0;right:0;display:block;width:48px;height:48px;cursor:pointer;text-indent:-99999px;background-image:url("https://assets.ubuntu.com/v1/12387180-navigation-menu-plain.svg");background-repeat:no-repeat;background-position:center center;background-size:25px auto}@media only screen and (min-width:860px){.banner .nav-toggle{display:none}}.banner .nav-toggle .nav-toggle__link{width:48px;height:48px}.banner .nav-toggle .nav-toggle__link.open{display:block}.banner .nav-toggle .nav-toggle__link.close{display:none}@media only screen and (max-width:860px){.banner #nav:hover ul{display:block}.banner #nav:hover ul ul{display:none}.banner #nav:hover .nav-toggle .nav-toggle__link.open{display:none}.banner #nav:hover .nav-toggle .nav-toggle__link.close{display:block}}ol,ul{margin-left:20px;margin-bottom:20px}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}nav ul,nav ol{list-style:none;list-style-image:none}li{font-size:1em;line-height:1.5;margin:0 0 .4em;padding:0}.list-ticks li{background-image:url("data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14'><circle fill='%2394310f' cx='7' cy='7' r='7'/><path fill='%23fff' d='M6.1 10.813L2.41 8.105l1.184-1.613L5.9 8.187l4.393-4.394 1.414 1.414z' /></svg>")}.no-bullets{list-style:none;margin-left:0}.combined-list ul,.combined-list div{margin-bottom:0}.combined-list .last-item{border-bottom:1px dotted #888;padding-bottom:10px}.combined-list .last-col{margin-bottom:20px}.combined-list .last-col .last-item{border-bottom:0;padding-bottom:0}@media only screen and (min-width:768px){.combined-list ul,.combined-list div{margin-bottom:20px}.combined-list .last-item{border-bottom:0;padding-bottom:0}}.inline-list{margin-left:0}.inline-list li{display:inline;list-style:none;margin-right:20px}.inline-list .last-item{margin-right:0}.list-step{list-style:none;margin-left:60px}@media only screen and (max-width:984px){.list-step__item:first-child{margin-top:10px}}@media only screen and (min-width:984px){.list-step__title{margin-bottom:0}}.list-step__bullet{box-shadow:0 1px 3px 1px rgba(51,51,51,0.2);background:#fff;border-radius:50%;padding:.3em .68em;display:inline-block;color:#e95420;margin-right:.34375em;margin-bottom:.625em;margin-left:-60px}@media only screen and (max-width:984px){.list-step__bullet{position:absolute;top:-5px}}@media only screen and (min-width:768px){.grid-list{display:flex;flex-wrap:wrap}}.grid-list__img{display:block;margin:auto;max-width:60px}.grid-list p{font-size:.875rem}.grid-list__item{border-bottom:1px dotted #888;margin-bottom:30px;display:flex}@media only screen and (max-width:768px){.grid-list__item>[class*='-col']:first-child{width:25%;padding-right:1rem}.grid-list__item>[class*='-col']:last-child{width:75%}}.grid-list__item:last-child{border-bottom:0}@media only screen and (min-width:768px){.grid-list__item{display:flex;flex-wrap:wrap;border-right:1px dotted #888;margin:0;padding:1.5em .75em 0}.grid-list__item.last-col{border-right:0}.grid-list__item.last-row{border-bottom:0}}.row{border-bottom:1px dotted #888;clear:both;padding:20px 10px 0;position:relative}.row br{display:none}.row.no-padding-bottom{padding-bottom:0}.row::after{clear:both;content:'.';display:block;height:0;visibility:hidden}.row--light{background:#fff}.row--dark{background:#333;color:#fff}.row.row-grey,.row.row--grey{background:#f7f7f7;border:0;margin-top:-1px}.no-border{border:0}.row-hero{margin-top:20px;padding-top:0}.strip{width:100%;display:block}.strip-inner-wrapper{max-width:984px;margin:auto}@media only screen and (min-width:768px){.row-hero{margin-top:40px}.row{border-radius:0;margin:0;padding:40px 20px 20px}.row-grey{margin-top:-1px}}@media only screen and (min-width:769px){.row br{display:block}}@media only screen and (min-width:984px){.row br{display:block}.row{padding:60px 20px 40px}.no-border{border:0}}.header-search [type="search"],.header-search [type="text"],.search-form [type="search"],.search-form [type="text"]{-webkit-appearance:none;background-color:#c34113;box-shadow:none;color:#333;display:block;float:left;font-size:1em;margin-bottom:0;padding:12px 10px;transition:all .5s ease-out;width:100%}.header-search .svg-search-handle,.search-form .svg-search-handle{fill:#333}.header-search .svg-search-frame,.search-form .svg-search-frame{stroke:#333}.header-search placeholder,.search-form placeholder{color:#333;opacity:1}.header-search input:placeholder,.search-form input:placeholder{color:#333;opacity:1}.header-search ::placeholder,.search-form ::placeholder{color:#333;opacity:1}.header-search [type="search"]:focus,.search-form [type="search"]:focus{background-color:#b03a11;border-color:#b3b3b3}.header-search [type=submit],.search-form [type=submit]{background:0;display:block;float:left;line-height:0;margin-left:-40px;overflow:visible;padding:3px 2px;width:auto}.header-search [type=submit]:hover,.search-form [type=submit]:hover{background:0}.header-search [type=submit] img,.search-form [type=submit] img{height:28px;width:28px;margin-top:2px}.banner .search-toggle{background-image:url("data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 90 90'><g color='%23fff'><path fill='none' stroke-width='4' overflow='visible' enable-background='accumulate' d='M0 0h90v90H0z'/><path d='M69 36.5a33 33.5 0 1 1-66 0 33 33.5 0 1 1 66 0z' transform='matrix(.636 0 0 .627 16.114 16.12)' fill='none' stroke='%23fff' stroke-width='9.5' overflow='visible' enable-background='accumulate'/><path d='M55.77 52.92L52.94 55.75l14 14 2.83-2.83-14-14z' font-size='xx-small' fill='%23fff' stroke-width='6' overflow='visible' enable-background='accumulate' font-family='Sans' class='s0'/><path d='M60.97 57.03c-1.55-3.04 1.02-3.63 2.46-0.58 1.44-0.21 3.21 4.29l9.19 9.2c2.68 2.85 3.26 2.46 5.64 2.38-2.38 2.77-2.79-0.08-5.65l-9.19-9.2c-0.73-0.75-1.78-1.19-2.83-1.19z' font-size='xx-small' fill='%23fff' stroke-width='11.8' overflow='visible' enable-background='accumulate' font-family='Sans' class='s0'/></g></svg>");background-position:center center;background-repeat:no-repeat;background-size:20px 20px;display:block;height:48px;outline:0;overflow:hidden;position:absolute;right:0;text-indent:-999em;top:0;width:24px}.banner .search-toggle .search-toggle__link{width:48px;height:48px}.banner .search-toggle .search-toggle__link.open{display:block}.banner .search-toggle .search-toggle__link.close{display:none}#site-search:hover form{display:block}#site-search:hover .search-toggle .search-toggle__link.open{display:none}#site-search:hover .search-toggle .search-toggle__link.close{display:block}.header-search,.search-form{background:#f7f7f7;border:0;display:none;float:left;position:relative;margin:0;width:100%;z-index:3}.search-form.active,.header-search.active,.header-search.open{display:block}.search-form div,.header-search div{box-shadow:inset 0 -4px 4px -4px rgba(0,0,0,0.3),inset 0 5px 5px -5px rgba(0,0,0,0.3);background:#ec6d40;margin:10px;position:relative;z-index:1}.search-form form [type="search"],.header-search form [type="search"]{background:#fff;border:0;box-shadow:0 2px 2px rgba(0,0,0,0.3) inset,0 -1px 3px rgba(0,0,0,0.2) inset,0 2px 0 rgba(255,255,255,0.4);color:#fff;display:block;float:left;font-size:1em;height:auto;margin:0;padding:8px 10px;width:100%}.yes-js .header-inner .search-form,.yes-js .header-inner .header-search{display:none}.yes-js .header-inner .search-form form,.yes-js .header-inner .header-search form{margin-left:0;margin-right:0;overflow:hidden;padding:10px;position:relative;top:0;width:100%;z-index:999}@media only screen and (max-width:860px){.banner .search-toggle{right:0}.no-svg .search-toggle,.opera-mini .search-toggle{background-image:url("https://assets.ubuntu.com/v1/75d8151d-search-white.png")}}@media only screen and (min-width:860px){.banner .search-toggle{display:none}}@media only screen and (min-width:860px){.search-form,.header-search{background:0;border-right:0 none;float:right;margin-bottom:0;max-width:220px;overflow:hidden;padding:7px 0 5px 14px}.search-form form [type="text"],.search-form form [type="search"],.header-search form [type="text"],.header-search form [type="search"]{box-shadow:0 2px 4px rgba(0,0,0,0.4) inset;box-sizing:content-box;background-color:#c34113;border:0 solid #b93e12;border-width:0 0 1px;color:#fff;font-size:.813em;height:24px;margin-bottom:0;padding:.5em 2.5em .5em .5em;transition:all .5s ease 0s;width:86px}.search-form form [type="text"]:focus,.search-form form [type="search"]:focus,.header-search form [type="text"]:focus,.header-search form [type="search"]:focus{background-color:#b03a11;border-color:#94310f}.search-form .svg-search-handle,.header-search .svg-search-handle{fill:#fff}.search-form .svg-search-frame,.header-search .svg-search-frame{stroke:#fff}.search-form placeholder,.header-search placeholder{color:#fff}.search-form input:placeholder,.header-search input:placeholder{color:#fff}.search-form ::placeholder,.header-search ::placeholder{color:#fff}}@media only screen and (min-width:860px){.header-search .svg-search-handle,.search-form .svg-search-handle{fill:#fff}.header-search .svg-search-frame,.search-form .svg-search-frame{stroke:#fff}.header-search placeholder,.search-form placeholder{color:#fff}.header-search input:placeholder,.search-form input:placeholder{color:#fff}.header-search ::placeholder,.search-form ::placeholder{color:#fff}}@media only screen and (max-width:860px){.banner .nav-primary .header-search{position:relative;top:0;width:100%}.banner .nav-primary .header-search [type="search"]{background-color:#ebebeb;color:#fff}.banner .nav-primary .header-search [type="submit"]{background-color:transparent;height:38px;margin-top:0;width:32px}.banner .nav-primary .header-search [type="submit"] img,.banner .nav-primary .header-search [type="submit"] svg{max-width:none}.banner .nav-primary .header-search.open{display:block}.banner .search-toggle{background-image:url("data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 90 90'><g color='%23fff'><path fill='none' stroke-width='4' overflow='visible' enable-background='accumulate' d='M0 0h90v90H0z'/><path d='M69 36.5a33 33.5 0 1 1-66 0 33 33.5 0 1 1 66 0z' transform='matrix(.636 0 0 .627 16.114 16.12)' fill='none' stroke='%23fff' stroke-width='9.5' overflow='visible' enable-background='accumulate'/><path d='M55.77 52.92L52.94 55.75l14 14 2.83-2.83-14-14z' font-size='xx-small' fill='%23fff' stroke-width='6' overflow='visible' enable-background='accumulate' font-family='Sans' class='s0'/><path d='M60.97 57.03c-1.55-3.04 1.02-3.63 2.46-0.58 1.44-0.21 3.21 4.29l9.19 9.2c2.68 2.85 3.26 2.46 5.64 2.38-2.38 2.77-2.79-0.08-5.65l-9.19-9.2c-0.73-0.75-1.78-1.19-2.83-1.19z' font-size='xx-small' fill='%23fff' stroke-width='11.8' overflow='visible' enable-background='accumulate' font-family='Sans' class='s0'/></g></svg>");background-position:center center;background-repeat:no-repeat;background-size:25px auto;cursor:pointer;display:block;height:48px;position:absolute;right:0;text-indent:-99999px;width:48px}.no-svg .banner .search-toggle,.opera-mini .banner .search-toggle{background-image:url("https://assets.ubuntu.com/v1/2196e362-search-white.svg")}.opera-mini x:-o-prefocus,.opera-mini .banner .search-toggle{background-size:25px auto}}@media only screen and (min-width:860px){.search-form,.header-search{display:block}.search-form .svg-search-handle,.header-search .svg-search-handle{fill:#fff}.search-form .svg-search-frame,.header-search .svg-search-frame{stroke:#fff}.search-form form [type="text"]:focus,.header-search form [type="text"]:focus{width:160px}.search-form [type="search"],.search-form [type="text"],.header-search [type="search"],.header-search [type="text"]{padding:8px 10px}}@media only screen and (max-width:984px){.search-form,.header-search{margin-right:10px}}@media only screen and (max-width:860px){.banner .search-toggle{right:48px}}.ubuntu-search .nav-secondary,.search-results .nav-secondary,.search-no-results .nav-secondary{display:none}.ubuntu-search section>h1,.ubuntu-search section article h1,.search-results section>h1,.search-results section article h1,.search-no-results section>h1,.search-no-results section article h1{padding-bottom:10px;font-size:1.438em;margin-bottom:0}.ubuntu-search section>h1,.search-results section>h1,.search-no-results section>h1{border-bottom:1px dotted #cdcdcd}.ubuntu-search .main-search,.search-results .main-search,.search-no-results .main-search{background-color:transparent;margin:0 0 20px;padding:20px 0}.ubuntu-search .main-search [type="search"],.search-results .main-search [type="search"],.search-no-results .main-search [type="search"]{border:1px solid #888;float:left;font-size:2em;padding:.2em 65px .2em .2em;width:100%}.ubuntu-search .main-search [type=submit],.search-results .main-search [type=submit],.search-no-results .main-search [type=submit]{width:32px;height:38px;background-repeat:no-repeat;background-position:center 8px;background-color:transparent;background-size:28px 28px;display:block;float:left;line-height:0;margin-left:-53px;margin-top:-4px;overflow:visible;padding:4px}.ubuntu-search .main-search [type=submit] img,.search-results .main-search [type=submit] img,.search-no-results .main-search [type=submit] img{height:45px;width:45px}.ubuntu-search .main-search [type=submit]:hover,.search-results .main-search [type=submit]:hover,.search-no-results .main-search [type=submit]:hover{background:0}.ubuntu-search .search-result h1 .title-main,.search-results .search-result h1 .title-main,.search-no-results .search-result h1 .title-main{margin-right:20px}.ubuntu-search .search-result h1 .result-url,.search-results .search-result h1 .result-url,.search-no-results .search-result h1 .result-url{color:#888;display:block;overflow:hidden;padding-bottom:2px;text-overflow:ellipsis;vertical-align:bottom}.ubuntu-search .search-result h1 .result-url a,.search-results .search-result h1 .result-url a,.search-no-results .search-result h1 .result-url a{color:#888}.ubuntu-search .search-result p,.search-results .search-result p,.search-no-results .search-result p{margin-bottom:0}.ubuntu-search .num-results,.search-results .num-results,.search-no-results .num-results{display:inline-block;margin-left:20px}.ubuntu-search .bottom-results-total,.search-results .bottom-results-total,.search-no-results .bottom-results-total{margin:0;overflow:visible;padding-top:20px;text-align:center;width:100%}.ubuntu-search .bottom-nav,.search-results .bottom-nav,.search-no-results .bottom-nav{margin-top:-26px;overflow:hidden}.ubuntu-search .bottom-nav ul,.search-results .bottom-nav ul,.search-no-results .bottom-nav ul{margin-bottom:0;margin-left:0;overflow:hidden;padding:0}.ubuntu-search .bottom-nav li,.search-results .bottom-nav li,.search-no-results .bottom-nav li{float:left;margin-left:15px}.ubuntu-search .bottom-nav li:first-child,.search-results .bottom-nav li:first-child,.search-no-results .bottom-nav li:first-child{margin-left:0}.ubuntu-search .nav-back,.search-results .nav-back,.search-no-results .nav-back{float:left}.ubuntu-search .nav-back li:before,.search-results .nav-back li:before,.search-no-results .nav-back li:before{color:#e95420;content:'\2039';margin-right:5px}.ubuntu-search .nav-back .item-extreme:before,.search-results .nav-back .item-extreme:before,.search-no-results .nav-back .item-extreme:before{content:'\2039\2039'}.ubuntu-search .nav-forward,.search-results .nav-forward,.search-no-results .nav-forward{float:right}.ubuntu-search .nav-forward li:after,.search-results .nav-forward li:after,.search-no-results .nav-forward li:after{color:#e95420;content:'\203A';margin-left:5px}.ubuntu-search .nav-forward .item-extreme:after,.search-results .nav-forward .item-extreme:after,.search-no-results .nav-forward .item-extreme:after{content:'\203A\203A'}.ubuntu-search .error-notification,.search-results .error-notification,.search-no-results .error-notification{background-color:#fff;color:#333;display:block;margin-top:20px;padding:20px;width:100%}.ubuntu-search .result-line,.search-results .result-line,.search-no-results .result-line{color:#888}.ubuntu-search .results-top,.search-results .results-top,.search-no-results .results-top{border-bottom:1px dotted #cdcdcd;padding-bottom:.5em}.ubuntu-search .search-container,.search-results .search-container,.search-no-results .search-container{padding-bottom:0}@media only screen and (min-width:860px){.ubuntu-search .main-search [type=submit]{margin-left:-60px;margin-top:0}}@font-face{font-family:'Ubuntu';font-style:normal;font-weight:300;src:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-l-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-l-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:normal;font-weight:400;src:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-r-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-r-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:normal;font-weight:700;src:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-b-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-b-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:italic;font-weight:300;src:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-li-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-li-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:italic;font-weight:400;src:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-ri-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-ri-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:italic;font-weight:700;src:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-bi-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-bi-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu Mono';font-style:normal;font-weight:400;src:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntumono-r-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntumono-r-webfont.woff") format("woff")}*{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{color:#333;font-family:Ubuntu,Arial,'libra sans',sans-serif;font-size:16px;font-weight:300}a:focus{outline:thin dotted}a:hover,a:active{outline:0}a{color:#e95420;text-decoration:none}a:hover,a:active,a:focus{text-decoration:underline}strong{font-weight:400}.caps-centered,.muted-heading{font-size:.875em;margin-bottom:20px;text-align:center;text-transform:uppercase}small,.smaller{font-size:13px}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{vertical-align:text-top}sub{vertical-align:text-bottom}pre{border-radius:4px;background-color:white;padding:.6em 1em;white-space:pre-wrap;word-wrap:break-word}p+h2,ul+h2,ol+h2,pre+h2{margin-top:.5625em}header nav a:link{font-weight:normal}p+h3,ul+h3,ol+h3,pre+h3{margin-top:.783em}p+h4,ul+h4,ol+h4,pre+h4{margin-top:1.21875}ol+h2,p+h2,pre+h2,ul+h2{margin-top:.563em}ol+h3,p+h3,pre+h3,ul+h3{margin-top:.783em}ol+h4,p+h4,pre+h4,ul+h4{margin-top:1.219em}.intro{font-size:1em;line-height:1.4}.row div p:last-child,.row ul p:last-child{margin-bottom:0}.four-col p:last-child{margin-bottom:0}.note{color:#888;font-size:.813em}h1,h2,h3,h4,h5,h6{font-family:Ubuntu,Arial,'libra sans',sans-serif;font-weight:300;line-height:1.3}h1{font-size:2.8125em;margin-bottom:.5em}h2{font-size:2em;margin-bottom:.5em}h3{font-size:1.4375em;margin-bottom:.522em}h4{font-size:1.25em;font-weight:400;margin-bottom:.615em}h5{font-size:1em;font-weight:700;margin-bottom:1em}h6{font-size:.8125em;font-weight:400;margin-bottom:1em;letter-spacing:.1em;text-transform:uppercase}p,li{font-size:1em;line-height:1.5;margin:0;margin-bottom:.75em;padding:0}button,input,select,textarea{font-family:Ubuntu,Arial,'libra sans',sans-serif}@media only screen and (min-width:768px) and (max-width:984px){h1{font-size:1.5234375em;margin-bottom:.5em}h2{font-size:1.348125em;margin-bottom:.5em}h3{font-size:1.1428125em;margin-bottom:.522em}h4{font-size:1.171875em;font-weight:400;margin-bottom:.615em}h5{font-size:.9375em;font-weight:700;margin-bottom:1em}h6{font-size:.6778125em;font-weight:400;margin-bottom:1em;letter-spacing:.1em;text-transform:uppercase}.intro{font-size:1.13333em}}@media only screen and (max-width:768px){h1{font-size:1.421875em;margin-bottom:.4375em}h2{font-size:1.25825em;margin-bottom:.4375em}h3{font-size:1.066625em;margin-bottom:.45675em}h4{font-size:1.09375em;font-weight:400;margin-bottom:.538125em}h5{font-size:.875em;font-weight:700;margin-bottom:.875em}h6{font-size:.632625em;font-weight:400;margin-bottom:.875em;letter-spacing:.1em;text-transform:uppercase}p,li{font-size:.875rem}}@media only screen and (min-width:984px){p,li,code,pre{font-size:16px;line-height:1.5;margin-bottom:.75em}.intro{font-size:1.25em}}dfn{font-style:italic}code,pre{font-family:'Ubuntu Mono','Consolas','Monaco','Lucida Console','Courier New',Courier,monospace}table{border-collapse:collapse;border-spacing:0;overflow-x:scroll;margin-bottom:20px;margin:0 0 2.5em;width:100%}table th,table td{background:#f7f7f7;border:1px dotted #888;padding:15px 10px}table td{text-align:center;vertical-align:middle}table thead th{border-collapse:separate;border-spacing:0 10px;background:#fee3d2;color:#333;font-weight:normal}table tbody th{font-weight:300;text-align:left}table th[scope='col']{text-align:center}table thead th:first-of-type{text-align:left}@media only screen and (max-width:768px){table{display:block}}body footer.global #nav-global li:first-of-type a{margin-left:0}footer.global{background:0;border-top:0;box-shadow:inset 0 2px 2px -1px #cdcdcd;clear:both;display:block;padding:30px 10px 20px;position:relative;width:100%}footer.global .legal{background-image:none;clear:both;margin:0 auto;min-height:40px;position:relative;width:100%}footer.global .legal p,footer.global .legal ul{padding-left:0}footer.global h2{font-size:.75em;line-height:1.4;margin-bottom:0;padding-bottom:.5em}footer.global h2,footer.global h2 a:link,footer.global h2 a:visited{color:#333;font-weight:normal}footer.global ul{margin:0}footer.global li a:hover{color:#e95420}footer.global li ul li a:link,footer.global li ul li a:visited{color:#333;font-size:.75em}footer.global h2 a:hover,footer.global h2 a:active{color:#e95420}footer.global p{color:#333;font-size:12px;margin-bottom:0}@media only screen and (max-width:768px){footer.no-global .legal{box-shadow:0 2px 2px -1px #cdcdcd inset;box-sizing:content-box;margin-left:-10px;padding-left:10px;padding-right:10px;padding-top:10px}}@media only screen and (min-width:984px){footer.global .legal{width:984px}footer.global{padding:30px 0 20px}}svg:not(:root){overflow:hidden}figure{width:100%}figure caption{display:block;width:100%;text-align:center}object,iframe,embed,canvas,video,audio{display:block;max-width:100%;margin:0 auto 20px}audio:not([controls]){display:none;height:0}[hidden]{display:none}.video-container{position:relative;padding-bottom:56.25%;padding-top:30px;height:0;overflow:hidden}.video-container iframe{position:absolute;top:0;left:0;width:100%;height:100%}.video-container h3,.video-container .video-title{margin-top:20px}.inline-logos{float:left;margin-left:0;padding:0;text-align:center;width:100%}.inline-logos .inline-logos__item{display:inline-block;margin-right:20px;padding:0 0 20px;width:36%}@media only screen and (max-width:300px){.inline-logos .inline-logos__item{margin:0 0 20px;width:100%}}.inline-logos .inline-logos__item.clear-row{clear:left}.inline-logos .inline-logos__item.last-item{border:0}.inline-logos .inline-logos__image{max-height:32px;max-width:115px;transition:all .3s ease-out;vertical-align:middle}@media only screen and (min-width:768px){.inline-logos{padding-top:20px}.inline-logos .inline-logos__item{display:inline-block;height:56px;line-height:60px;margin:0 30px 30px;width:150px}.inline-logos .inline-logos__image{float:none;height:auto;max-height:56px;max-width:150px;vertical-align:middle}}@media only screen and (min-width:984px){.inline-logos__item{margin-bottom:40px}}@media only screen and (min-width:768px){.equal-height__align-vertically{align-items:center;justify-content:center}}@media only screen and (min-width:768px){.equal-height--vertical-divider{position:relative}.equal-height--vertical-divider__item{padding-left:10px;padding-right:10px}.equal-height--vertical-divider__item:before{content:'';position:absolute;right:-10px;top:0;height:100%;width:1px;border-right:1px dotted #888}.equal-height--vertical-divider .last-col,.equal-height--vertical-divider__item:last-of-type{padding-right:0}.equal-height--vertical-divider .last-col:before,.equal-height--vertical-divider__item:last-of-type:before{border-right:0}.equal-height--vertical-divider__item:first-of-type{padding-left:0}}@media only screen and (max-width:768px){.equal-height--vertical-divider .equal-height--vertical-divider__item{border-bottom:1px dotted #888;padding-bottom:20px}.equal-height--vertical-divider .equal-height--vertical-divider__item:last-of-type{border-bottom:0;padding-bottom:inherit}}body{background-color:#f7f7f7;background-image:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/img/backgrounds/image-background-paper.png?w=$breakpoint-medium");background-position:center top;background-repeat:repeat-y}@media only screen and (min-width:768px){body{background-image:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/img/backgrounds/image-background-paper.png")}}.wrapper{margin:0 auto;max-width:984px}.inner-wrapper{background:#fff;border-radius:4px;box-shadow:0 0 3px #cdcdcd;margin:10px 0 0;padding-bottom:0}@media only screen and (max-width:768px){.inner-wrapper{margin:10px 0 30px;padding-bottom:20px}}@media only screen and (max-width:985px){.inner-wrapper{margin:0}}@media only screen and (min-width:985px){.banner{margin-bottom:30px}}.banner .nav-primary ul{border:0}.banner .nav-primary ul li{border-left-color:#df4a16}.banner .nav-primary ul li ul{box-shadow:0 2px 2px -1px #888;border-radius:10px;background:#f7f7f7;border:1px solid #cdcdcd;display:none;float:none;margin:0;padding:5px 0 20px;position:absolute;top:51px;width:200px}.banner .nav-primary ul li a,.banner .nav-primary ul li a:link,.banner .nav-primary ul li a:visited,.banner .nav-primary ul li a.active:link,.banner .nav-primary ul li a.active:visited{font-weight:normal}@media only screen and (min-width:768px){.banner .nav-primary ul li a,.banner .nav-primary ul li a:link,.banner .nav-primary ul li a:visited,.banner .nav-primary ul li a.active:link,.banner .nav-primary ul li a.active:visited{border-left:1px solid #eb6233}}.banner .nav-primary ul li a:hover,.banner .nav-primary ul li a:link:hover,.banner .nav-primary ul li a:visited:hover,.banner .nav-primary ul li a.active:link:hover,.banner .nav-primary ul li a.active:visited:hover{background:#ec693c;box-shadow:none;color:#fff}@media only screen and (min-width:860px){.banner .nav-primary ul ul{display:none}}.banner .nav-primary ul ul li{border:0}.banner .nav-primary ul ul li:last-child{border:0}.banner .nav-primary ul li ul li a,.banner .nav-primary ul li ul li a:link,.banner .nav-primary ul li ul li a:visited{color:#333;border:0;padding:10px 0 0 14px;font-weight:normal;text-align:left;width:186px}.banner .nav-primary ul .second-level-nav li a:link:hover,.banner .nav-primary ul .second-level-nav li a:visited:hover{background:0;color:#e95420}.banner .nav-primary ul .second-level-nav .active:link,.banner .nav-primary ul .second-level-nav .active:visited,.banner .nav-primary ul .second-level-nav .active:link:hover,.banner .nav-primary ul .second-level-nav .active:visited:hover{background:0;border:0;color:#333}.banner .nav-primary ul li:hover ul::after{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QYODgYVPPJJpQAAAT9JREFUKM+dkD9IAnEUx7+/IkVxFaxoKYxIWhqKwJw0JAJ315Yac8+1tUFo6HCp34XRdRp11z+J4BzckqShPzdEQ5G/EhPO6V5Tcg3G0QcePHjvffjygB4oXAIApFMxyPlcWM7nwulU7NfMNQqXGABsZNcCCpcKtapBtapBCpcK2fXlAAAcy3vMlUxTdgEAO9ubybPiPjWFoE6nQ5ZlUfNDULl0RAdSPuncddL30+gnCgOAlngLaocyn4nG9fmFJXj9fhARAMDr82MuEcfk7LSuFQu8DTvovO1SuTxlAKCqPFO5Ov/6FIIsy/qzROOdrst6S1V5xunomo0LrT4yOh4JDg6BMXfvIdvGs/mExutLPZpYnAIAdmNUVuwBbI1NRODxePEf2u0WHu9u4ev3rTLTfKBQaNh1qp5piWCa9/gGBheo3r6AmYcAAAAASUVORK5CYII=") no-repeat;content:'';display:block;height:8px;left:20px;position:relative;top:-13px;width:200px;z-index:999}.banner .nav-primary ul li:hover ul{display:block}@media only screen and (min-width:985px){.banner .logo{margin-left:0}}.strip-dark{background-color:#2c001e;background-image:url("https://assets.ubuntu.com/v1/62d597f6-background-grid.png");color:#fff}.strip-dark .resource{box-shadow:none;color:#333;padding-right:20px}.strip-dark .resource::before{border-bottom-width:29px;border-right-color:#2c001e;right:-2px;top:-1px}.strip-dark .resource:hover::before{border-bottom-width:34px}.context-footer{box-sizing:border-box;clear:both;display:block;padding:0 10px;position:relative;width:100%}.context-footer hr{background:#e95420;border:0;box-shadow:inset 0 2px 2px -2px #888;clear:both;height:14px;margin:0 -10px 20px}.context-footer div>div{border-bottom:1px dotted #888;padding-bottom:20px}.context-footer div>div:last-child{border:0;padding-bottom:0}.context-footer ul{margin-bottom:5px}.context-footer h3{font-size:1em;font-weight:normal;margin-bottom:.75em}@media only screen and (min-width:768px){.context-footer div>div{border:0;padding-bottom:0}.context-footer hr{margin:0 -30px 40px}.context-footer h3{font-size:1.07143125em}.context-footer p,.context-footer li,.context-footer a{font-size:.9375em}}@media only screen and (min-width:984px){.context-footer{padding:0 20px}.context-footer hr{margin:0 -20px 40px}.context-footer h3{font-size:1.14286em}.context-footer p,.context-footer li,.context-footer a{font-size:1em}}.top-link{position:absolute;left:-999em}.pull-left-20{margin-left:-20px}.pull-right-20{margin-right:-20px}.pull-left-40{margin-left:-40px}.pull-right-40{margin-right:-41px}.arrow-up{background-image:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/patterns/arrow-up.png");left:20px;top:-11px}.arrow-down{background-image:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/patterns/arrow-down.png");bottom:-11px;right:20px}.arrow-right{background-image:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/patterns/arrow-right.png");height:18px;right:-11px;top:20px;width:11px}.arrow-left{background-image:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/patterns/arrow-left.png");bottom:20px;height:18px;left:-11px;width:11px}.box{background:#fff;border:1px solid #cdcdcd}.box-grey{background:#f7f7f7;color:#333}.box-orange{background:#e95420;color:#fff}.box-highlight{border:1px solid #f7f7f7;box-shadow:0 2px 2px 0 #cdcdcd}.box-textured{background:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/patterns/grey-textured-background.jpg");border:0;box-shadow:0 2px 2px 0 #cdcdcd}.box-padded{background:#efefef;border:0;border-radius:4px;margin-bottom:20px;padding:6px 5px}.box-padded h3{font-size:1.21875em;margin-left:5px;margin-top:5px}.box-padded li h3{font-size:1.21875em;margin:0}.box-padded div{background:#fff;border-radius:4px;overflow:hidden;padding:8px 8px 2px}.box-padded-feature{background:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/patterns/soft-centre-bkg.gif") repeat scroll 0 0 #888;border-radius:4px;border:0;margin-bottom:20px;padding:11px 5px 6px}.box-padded-feature h3{color:#fff;font-size:1.21875em;margin-left:5px}.box-padded-feature h4{font-size:1em;font-weight:normal}.box-padded-feature>div{background:#fff;border-radius:4px;overflow:hidden;padding:20px 8px}.box-padded-feature div div{margin-bottom:0}.box-padded-feature .one-col{float:left;width:48px}@media only screen and (max-width:768px){.box-padded-feature .inline-icons li{display:block;float:left}.box-padded-feature .one-col{float:left;width:48px}}.resource{cursor:pointer;padding-bottom:40px;position:relative;transition:background .2s ease-out}.resource.four-col h2 a:link,.resource.four-col h2 a:visited{font-size:1.125em}.resource.twelve-col h2 a:link,.resource.twelve-col h2 a:visited{font-size:1.40625em}.resource:hover{background-color:#fafafa}.resource::after{box-shadow:0 -1px 2px 0 #ddd;content:'';height:1px;position:absolute;right:-6px;top:14px;transform:rotate(45deg);transition:all .2s ease-out;width:41px;z-index:2}.resource:hover::after{right:-9px;top:18px;width:48px}.resource::before{border-bottom:30px solid #f7f7f7;border-radius:0;border-right:30px solid #fff;box-shadow:-2px 2px 2px rgba(247,247,247,0.4);content:'';height:0;position:absolute;right:-3px;top:-2px;transition:border-width .2s ease-out;width:0;z-index:2}.resource:hover::before{border-bottom-width:35px;border-right-width:35px}.resource:last-of-type{margin-bottom:30px}.resource .content-cat{background:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/icons/icon-resource-hub-icon-document.png") left center no-repeat;color:#888;font-size:14px;letter-spacing:1px;margin:0;padding:0 0 0 20px;position:absolute;text-transform:uppercase}.resource .content-cat-webinar{background:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/icons/icon-resource-hub-webinar.png") left center no-repeat}.resource.box-image-centered div+span img{margin-top:40px}.resource h2{padding-right:20px}.row-grey .resource::before{border-right-color:#f7f7f7}.list-ticks--compact li{background-image:url("data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14'><circle fill='%2394310f' cx='7' cy='7' r='7'/><path fill='%23fff' d='M6.1 10.813L2.41 8.105l1.184-1.613L5.9 8.187l4.393-4.394 1.414 1.414z' /></svg>")}.list-ticks--canonical li{background-image:url("data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14'><circle fill='%23772953' cx='7' cy='7' r='7'/><path fill='%23fff' d='M6.1 10.813L2.41 8.105l1.184-1.613L5.9 8.187l4.393-4.394 1.414 1.414z' /></svg>")}.list-ticks--canonical-compact li{background-image:url("data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14'><circle fill='%23772953' cx='7' cy='7' r='7'/><path fill='%23fff' d='M6.1 10.813L2.41 8.105l1.184-1.613L5.9 8.187l4.393-4.394 1.414 1.414z' /></svg>")}@media only screen and (min-width:860px){.nav-secondary{border-width:0 0 1px}.nav-secondary .breadcrumb{padding-top:8px;padding-bottom:7px}}@media only screen and (min-width:860px){.header-search [type="search"],.header-search [type="text"],.search-form [type="search"],.search-form [type="text"]{border-radius:4px}}blockquote{padding:inherit 1em}.collapse{cursor:pointer;display:block}.collapse+input{display:none}.collapse+input+div{display:none}.collapse+input:checked+div{display:block}.cheshire{display:none}
783\ No newline at end of file
784+[grid-demo] [class*="col-"]{background:#cdcdcd;margin-bottom:1rem}[grid-demo] [class*="col-"]{background:#cdcdcd;margin-bottom:1rem}@media screen and (max-width:400px){@-ms-viewport{width:320px}}img{max-width:100%;height:auto}@media \0screen{img{width:auto}}.row{*zoom:1;margin-right:auto;margin-left:auto;max-width:1030px;padding-left:20px;padding-right:20px}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row .row{margin-right:0;margin-left:0;max-width:none;padding-right:0;padding-left:0}.mobile-col-1,.mobile-col-2,.mobile-col-3{display:block;float:left;min-height:1px;position:relative;*margin-right:-1px;margin-left:4.61165%}.row .mobile-col-1:first-child,.row .mobile-col-2:first-child,.row .mobile-col-3:first-child,.first-mobile-col{margin-left:0}.mobile-col-1{width:21.54126%}.mobile-col-2{width:47.69417%}.mobile-col-3{width:73.84709%}@media screen and (min-width:620px){.tablet-col-1,.tablet-col-2,.tablet-col-3,.tablet-col-4,.tablet-col-5{display:block;float:left;min-height:1px;position:relative;*margin-right:-1px;margin-left:2.91262%}.row .tablet-col-1:first-child,.row .tablet-col-2:first-child,.row .tablet-col-3:first-child,.row .tablet-col-4:first-child,.row .tablet-col-5:first-child,.first-tablet-col{margin-left:0}.tablet-col-1{width:14.23948%}.tablet-col-2{width:31.39159%}.tablet-col-3{width:48.54369%}.tablet-col-4{width:65.69579%}.tablet-col-5{width:82.8479%}}@media screen and (min-width:768px){.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11{display:block;float:left;min-height:1px;position:relative;*margin-right:-1px;margin-left:1.94175%}.row .col-1:first-child,.row .col-2:first-child,.row .col-3:first-child,.row .col-4:first-child,.row .col-5:first-child,.row .col-6:first-child,.row .col-7:first-child,.row .col-8:first-child,.row .col-9:first-child,.row .col-10:first-child,.row .col-11:first-child,.first-col{margin-left:0}.col-1{width:6.5534%}.col-2{width:15.04854%}.col-3{width:23.54369%}.col-4{width:32.03883%}.col-5{width:40.53398%}.col-6{width:49.02913%}.col-7{width:57.52427%}.col-8{width:66.01942%}.col-9{width:74.51456%}.col-10{width:83.00971%}.col-11{width:91.50485%}.prefix-1{padding-left:8.49515%}.prefix-2{padding-left:16.99029%}.prefix-3{padding-left:25.48544%}.prefix-4{padding-left:33.98058%}.prefix-5{padding-left:42.47573%}.prefix-6{padding-left:50.97087%}.prefix-7{padding-left:59.46602%}.prefix-8{padding-left:67.96117%}.prefix-9{padding-left:76.45631%}.prefix-10{padding-left:84.95146%}.prefix-11{padding-left:93.4466%}.suffix-1{padding-right:8.49515%}.suffix-2{padding-right:16.99029%}.suffix-3{padding-right:25.48544%}.suffix-4{padding-right:33.98058%}.suffix-5{padding-right:42.47573%}.suffix-6{padding-right:50.97087%}.suffix-7{padding-right:59.46602%}.suffix-8{padding-right:67.96117%}.suffix-9{padding-right:76.45631%}.suffix-10{padding-right:84.95146%}.suffix-11{padding-right:93.4466%}.push-1{left:8.49515%}.push-2{left:16.99029%}.push-3{left:25.48544%}.push-4{left:33.98058%}.push-5{left:42.47573%}.push-6{left:50.97087%}.push-7{left:59.46602%}.push-8{left:67.96117%}.push-9{left:76.45631%}.push-10{left:84.95146%}.push-11{left:93.4466%}.pull-1{right:8.49515%}.pull-2{right:16.99029%}.pull-3{right:25.48544%}.pull-4{right:33.98058%}.pull-5{right:42.47573%}.pull-6{right:50.97087%}.pull-7{right:59.46602%}.pull-8{right:67.96117%}.pull-9{right:76.45631%}.pull-10{right:84.95146%}.pull-11{right:93.4466%}.col-11 .col-1,.col-11 .col-2,.col-11 .col-3,.col-11 .col-4,.col-11 .col-5,.col-11 .col-6,.col-11 .col-7,.col-11 .col-8,.col-11 .col-9,.col-11 .col-10{margin-left:2.12202%}.col-11 .col-1{width:7.1618%}.col-11 .col-2{width:16.44562%}.col-11 .col-3{width:25.72944%}.col-11 .col-4{width:35.01326%}.col-11 .col-5{width:44.29708%}.col-11 .col-6{width:53.5809%}.col-11 .col-7{width:62.86472%}.col-11 .col-8{width:72.14854%}.col-11 .col-9{width:81.43236%}.col-11 .col-10{width:90.71618%}.col-10 .col-1,.col-10 .col-2,.col-10 .col-3,.col-10 .col-4,.col-10 .col-5,.col-10 .col-6,.col-10 .col-7,.col-10 .col-8,.col-10 .col-9{margin-left:2.33918%}.col-10 .col-1{width:7.89474%}.col-10 .col-2{width:18.12865%}.col-10 .col-3{width:28.36257%}.col-10 .col-4{width:38.59649%}.col-10 .col-5{width:48.83041%}.col-10 .col-6{width:59.06433%}.col-10 .col-7{width:69.29825%}.col-10 .col-8{width:79.53216%}.col-10 .col-9{width:89.76608%}.col-9 .col-1,.col-9 .col-2,.col-9 .col-3,.col-9 .col-4,.col-9 .col-5,.col-9 .col-6,.col-9 .col-7,.col-9 .col-8{margin-left:2.60586%}.col-9 .col-1{width:8.79479%}.col-9 .col-2{width:20.19544%}.col-9 .col-3{width:31.59609%}.col-9 .col-4{width:42.99674%}.col-9 .col-5{width:54.39739%}.col-9 .col-6{width:65.79805%}.col-9 .col-7{width:77.1987%}.col-9 .col-8{width:88.59935%}.col-8 .col-1,.col-8 .col-2,.col-8 .col-3,.col-8 .col-4,.col-8 .col-5,.col-8 .col-6,.col-8 .col-7{margin-left:2.94118%}.col-8 .col-1{width:9.92647%}.col-8 .col-2{width:22.79412%}.col-8 .col-3{width:35.66176%}.col-8 .col-4{width:48.52941%}.col-8 .col-5{width:61.39706%}.col-8 .col-6{width:74.26471%}.col-8 .col-7{width:87.13235%}.col-7 .col-1,.col-7 .col-2,.col-7 .col-3,.col-7 .col-4,.col-7 .col-5,.col-7 .col-6{margin-left:3.37553%}.col-7 .col-1{width:11.39241%}.col-7 .col-2{width:26.16034%}.col-7 .col-3{width:40.92827%}.col-7 .col-4{width:55.6962%}.col-7 .col-5{width:70.46414%}.col-7 .col-6{width:85.23207%}.col-6 .col-1,.col-6 .col-2,.col-6 .col-3,.col-6 .col-4,.col-6 .col-5{margin-left:3.9604%}.col-6 .col-1{width:13.36634%}.col-6 .col-2{width:30.69307%}.col-6 .col-3{width:48.0198%}.col-6 .col-4{width:65.34653%}.col-6 .col-5{width:82.67327%}.col-5 .col-1,.col-5 .col-2,.col-5 .col-3,.col-5 .col-4{margin-left:4.79042%}.col-5 .col-1{width:16.16766%}.col-5 .col-2{width:37.12575%}.col-5 .col-3{width:58.08383%}.col-5 .col-4{width:79.04192%}.col-4 .col-1,.col-4 .col-2,.col-4 .col-3{margin-left:6.06061%}.col-4 .col-1{width:20.45455%}.col-4 .col-2{width:46.9697%}.col-4 .col-3{width:73.48485%}.col-3 .col-1,.col-3 .col-2{margin-left:8.24742%}.col-3 .col-1{width:27.83505%}.col-3 .col-2{width:63.91753%}.col-2 .col-1{margin-left:12.90323%}.col-2 .col-1{width:43.54839%}}.row .center-col{float:none;margin-left:auto !important;margin-right:auto}@media screen and (max-width:619px){.hidden-mobile,.visible-tablet,.visible-desktop{display:none !important}}@media screen and (min-width:620px) and (max-width:767px){.visible-mobile,.hidden-tablet,.visible-desktop{display:none !important}}@media screen and (min-width:768px){.visible-mobile,.visible-tablet,.hidden-desktop{display:none !important}}[class*="col-"],.row{margin-top:0}@media screen and (max-width:767px){[class*="col-"]+[class*="col-"]{margin-top:20px}}.p-matrix__item::after,.p-navigation--light::after,.p-navigation::after,.p-navigation--dark::after,.p-inline-images::after,.u-clearfix::after{clear:both;content:'';display:block}html{font-family:sans-serif;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:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{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}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:0;text-decoration:underline;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:inherit;margin:0}optgroup{font-weight:bold}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{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{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"]{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}blockquote{border-left:2px solid #666}blockquote>cite{display:block}html{box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}button{transition-duration:.165s;transition-property:background-color;transition-timing-function:cubic-bezier(0.55,0.055,0.675,0.19);background-color:#fff;border-color:#cdcdcd;border-radius:.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#111;cursor:pointer;display:inline-block;font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-smoothing:subpixel-antialiased;font-weight:300;line-height:1rem;outline:0;padding:.75rem 1.5rem;text-align:center;text-decoration:none;width:100%;line-height:1rem}@media only screen and (min-width:768px){button{width:auto}}button:visited{color:#111}button:active,button:focus,button:hover{background-color:#f7f7f7;border-color:#cdcdcd;text-decoration:none}button:disabled,button.is--disabled{cursor:not-allowed;opacity:.5}button:disabled:active,button:disabled:focus,button:disabled:hover,button.is--disabled:active,button.is--disabled:focus,button.is--disabled:hover{background-color:transparent;border-color:#cdcdcd}button .p-link--external{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}label{cursor:pointer;display:block;font-size:1rem;line-height:1.5}label.has-error{color:#c7162b}label.has-caution{color:#f99b11}label.has-warning{color:#f99b11}label.has-success{color:#0e8420}label.has-information{color:#335280}input[type='text'],input[type='date'],input[type='datetime'],input[type='datatime-local'],input[type='month'],input[type='time'],input[type='week'],input[type='color'],input[type='number'],input[type='search'],input[type='password'],input[type='email'],input[type='url'],input[type='tel']{appearance:textfield;background-color:#fff;border:1px solid #cdcdcd;border-radius:.125rem;box-shadow:inset 0 1px 2px rgba(0,0,0,0.12);font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-weight:300;outline:0;padding:.8rem .5333rem;vertical-align:baseline;width:100%}@media(min-width:768px){input[type='text'],input[type='date'],input[type='datetime'],input[type='datatime-local'],input[type='month'],input[type='time'],input[type='week'],input[type='color'],input[type='number'],input[type='search'],input[type='password'],input[type='email'],input[type='url'],input[type='tel']{padding:.75rem .5rem}}input[type='text']:active,input[type='text']:focus,input[type='date']:active,input[type='date']:focus,input[type='datetime']:active,input[type='datetime']:focus,input[type='datatime-local']:active,input[type='datatime-local']:focus,input[type='month']:active,input[type='month']:focus,input[type='time']:active,input[type='time']:focus,input[type='week']:active,input[type='week']:focus,input[type='color']:active,input[type='color']:focus,input[type='number']:active,input[type='number']:focus,input[type='search']:active,input[type='search']:focus,input[type='password']:active,input[type='password']:focus,input[type='email']:active,input[type='email']:focus,input[type='url']:active,input[type='url']:focus,input[type='tel']:active,input[type='tel']:focus{border-color:#666;color:#111;outline:0}input[type='text']::placeholder,input[type='date']::placeholder,input[type='datetime']::placeholder,input[type='datatime-local']::placeholder,input[type='month']::placeholder,input[type='time']::placeholder,input[type='week']::placeholder,input[type='color']::placeholder,input[type='number']::placeholder,input[type='search']::placeholder,input[type='password']::placeholder,input[type='email']::placeholder,input[type='url']::placeholder,input[type='tel']::placeholder{color:#666;opacity:1}input[type='text'][disabled],input[type='text'][disabled='disabled'],input[type='date'][disabled],input[type='date'][disabled='disabled'],input[type='datetime'][disabled],input[type='datetime'][disabled='disabled'],input[type='datatime-local'][disabled],input[type='datatime-local'][disabled='disabled'],input[type='month'][disabled],input[type='month'][disabled='disabled'],input[type='time'][disabled],input[type='time'][disabled='disabled'],input[type='week'][disabled],input[type='week'][disabled='disabled'],input[type='color'][disabled],input[type='color'][disabled='disabled'],input[type='number'][disabled],input[type='number'][disabled='disabled'],input[type='search'][disabled],input[type='search'][disabled='disabled'],input[type='password'][disabled],input[type='password'][disabled='disabled'],input[type='email'][disabled],input[type='email'][disabled='disabled'],input[type='url'][disabled],input[type='url'][disabled='disabled'],input[type='tel'][disabled],input[type='tel'][disabled='disabled']{cursor:not-allowed;opacity:.5}input[type='text'][readonly],input[type='text'][readonly='readonly'],input[type='date'][readonly],input[type='date'][readonly='readonly'],input[type='datetime'][readonly],input[type='datetime'][readonly='readonly'],input[type='datatime-local'][readonly],input[type='datatime-local'][readonly='readonly'],input[type='month'][readonly],input[type='month'][readonly='readonly'],input[type='time'][readonly],input[type='time'][readonly='readonly'],input[type='week'][readonly],input[type='week'][readonly='readonly'],input[type='color'][readonly],input[type='color'][readonly='readonly'],input[type='number'][readonly],input[type='number'][readonly='readonly'],input[type='search'][readonly],input[type='search'][readonly='readonly'],input[type='password'][readonly],input[type='password'][readonly='readonly'],input[type='email'][readonly],input[type='email'][readonly='readonly'],input[type='url'][readonly],input[type='url'][readonly='readonly'],input[type='tel'][readonly],input[type='tel'][readonly='readonly']{color:#cdcdcd;cursor:default}input[type='text'][readonly]:hover,input[type='text'][readonly]:active,input[type='text'][readonly]:focus,input[type='text'][readonly='readonly']:hover,input[type='text'][readonly='readonly']:active,input[type='text'][readonly='readonly']:focus,input[type='date'][readonly]:hover,input[type='date'][readonly]:active,input[type='date'][readonly]:focus,input[type='date'][readonly='readonly']:hover,input[type='date'][readonly='readonly']:active,input[type='date'][readonly='readonly']:focus,input[type='datetime'][readonly]:hover,input[type='datetime'][readonly]:active,input[type='datetime'][readonly]:focus,input[type='datetime'][readonly='readonly']:hover,input[type='datetime'][readonly='readonly']:active,input[type='datetime'][readonly='readonly']:focus,input[type='datatime-local'][readonly]:hover,input[type='datatime-local'][readonly]:active,input[type='datatime-local'][readonly]:focus,input[type='datatime-local'][readonly='readonly']:hover,input[type='datatime-local'][readonly='readonly']:active,input[type='datatime-local'][readonly='readonly']:focus,input[type='month'][readonly]:hover,input[type='month'][readonly]:active,input[type='month'][readonly]:focus,input[type='month'][readonly='readonly']:hover,input[type='month'][readonly='readonly']:active,input[type='month'][readonly='readonly']:focus,input[type='time'][readonly]:hover,input[type='time'][readonly]:active,input[type='time'][readonly]:focus,input[type='time'][readonly='readonly']:hover,input[type='time'][readonly='readonly']:active,input[type='time'][readonly='readonly']:focus,input[type='week'][readonly]:hover,input[type='week'][readonly]:active,input[type='week'][readonly]:focus,input[type='week'][readonly='readonly']:hover,input[type='week'][readonly='readonly']:active,input[type='week'][readonly='readonly']:focus,input[type='color'][readonly]:hover,input[type='color'][readonly]:active,input[type='color'][readonly]:focus,input[type='color'][readonly='readonly']:hover,input[type='color'][readonly='readonly']:active,input[type='color'][readonly='readonly']:focus,input[type='number'][readonly]:hover,input[type='number'][readonly]:active,input[type='number'][readonly]:focus,input[type='number'][readonly='readonly']:hover,input[type='number'][readonly='readonly']:active,input[type='number'][readonly='readonly']:focus,input[type='search'][readonly]:hover,input[type='search'][readonly]:active,input[type='search'][readonly]:focus,input[type='search'][readonly='readonly']:hover,input[type='search'][readonly='readonly']:active,input[type='search'][readonly='readonly']:focus,input[type='password'][readonly]:hover,input[type='password'][readonly]:active,input[type='password'][readonly]:focus,input[type='password'][readonly='readonly']:hover,input[type='password'][readonly='readonly']:active,input[type='password'][readonly='readonly']:focus,input[type='email'][readonly]:hover,input[type='email'][readonly]:active,input[type='email'][readonly]:focus,input[type='email'][readonly='readonly']:hover,input[type='email'][readonly='readonly']:active,input[type='email'][readonly='readonly']:focus,input[type='url'][readonly]:hover,input[type='url'][readonly]:active,input[type='url'][readonly]:focus,input[type='url'][readonly='readonly']:hover,input[type='url'][readonly='readonly']:active,input[type='url'][readonly='readonly']:focus,input[type='tel'][readonly]:hover,input[type='tel'][readonly]:active,input[type='tel'][readonly]:focus,input[type='tel'][readonly='readonly']:hover,input[type='tel'][readonly='readonly']:active,input[type='tel'][readonly='readonly']:focus{border-color:#666;outline:0}input[type='text'].has-error,input[type='date'].has-error,input[type='datetime'].has-error,input[type='datatime-local'].has-error,input[type='month'].has-error,input[type='time'].has-error,input[type='week'].has-error,input[type='color'].has-error,input[type='number'].has-error,input[type='search'].has-error,input[type='password'].has-error,input[type='email'].has-error,input[type='url'].has-error,input[type='tel'].has-error{border:1px solid #c7162b}input[type='text'].has-error:focus,input[type='date'].has-error:focus,input[type='datetime'].has-error:focus,input[type='datatime-local'].has-error:focus,input[type='month'].has-error:focus,input[type='time'].has-error:focus,input[type='week'].has-error:focus,input[type='color'].has-error:focus,input[type='number'].has-error:focus,input[type='search'].has-error:focus,input[type='password'].has-error:focus,input[type='email'].has-error:focus,input[type='url'].has-error:focus,input[type='tel'].has-error:focus{border:1px solid #c7162b}input[type='text'].has-caution,input[type='date'].has-caution,input[type='datetime'].has-caution,input[type='datatime-local'].has-caution,input[type='month'].has-caution,input[type='time'].has-caution,input[type='week'].has-caution,input[type='color'].has-caution,input[type='number'].has-caution,input[type='search'].has-caution,input[type='password'].has-caution,input[type='email'].has-caution,input[type='url'].has-caution,input[type='tel'].has-caution{border:1px solid #f99b11}input[type='text'].has-caution:focus,input[type='date'].has-caution:focus,input[type='datetime'].has-caution:focus,input[type='datatime-local'].has-caution:focus,input[type='month'].has-caution:focus,input[type='time'].has-caution:focus,input[type='week'].has-caution:focus,input[type='color'].has-caution:focus,input[type='number'].has-caution:focus,input[type='search'].has-caution:focus,input[type='password'].has-caution:focus,input[type='email'].has-caution:focus,input[type='url'].has-caution:focus,input[type='tel'].has-caution:focus{border:1px solid #f99b11}input[type='text'].has-warning,input[type='date'].has-warning,input[type='datetime'].has-warning,input[type='datatime-local'].has-warning,input[type='month'].has-warning,input[type='time'].has-warning,input[type='week'].has-warning,input[type='color'].has-warning,input[type='number'].has-warning,input[type='search'].has-warning,input[type='password'].has-warning,input[type='email'].has-warning,input[type='url'].has-warning,input[type='tel'].has-warning{border:1px solid #f99b11}input[type='text'].has-warning:focus,input[type='date'].has-warning:focus,input[type='datetime'].has-warning:focus,input[type='datatime-local'].has-warning:focus,input[type='month'].has-warning:focus,input[type='time'].has-warning:focus,input[type='week'].has-warning:focus,input[type='color'].has-warning:focus,input[type='number'].has-warning:focus,input[type='search'].has-warning:focus,input[type='password'].has-warning:focus,input[type='email'].has-warning:focus,input[type='url'].has-warning:focus,input[type='tel'].has-warning:focus{border:1px solid #f99b11}input[type='text'].has-success,input[type='date'].has-success,input[type='datetime'].has-success,input[type='datatime-local'].has-success,input[type='month'].has-success,input[type='time'].has-success,input[type='week'].has-success,input[type='color'].has-success,input[type='number'].has-success,input[type='search'].has-success,input[type='password'].has-success,input[type='email'].has-success,input[type='url'].has-success,input[type='tel'].has-success{border:1px solid #0e8420}input[type='text'].has-success:focus,input[type='date'].has-success:focus,input[type='datetime'].has-success:focus,input[type='datatime-local'].has-success:focus,input[type='month'].has-success:focus,input[type='time'].has-success:focus,input[type='week'].has-success:focus,input[type='color'].has-success:focus,input[type='number'].has-success:focus,input[type='search'].has-success:focus,input[type='password'].has-success:focus,input[type='email'].has-success:focus,input[type='url'].has-success:focus,input[type='tel'].has-success:focus{border:1px solid #0e8420}input[type='text'].has-information,input[type='date'].has-information,input[type='datetime'].has-information,input[type='datatime-local'].has-information,input[type='month'].has-information,input[type='time'].has-information,input[type='week'].has-information,input[type='color'].has-information,input[type='number'].has-information,input[type='search'].has-information,input[type='password'].has-information,input[type='email'].has-information,input[type='url'].has-information,input[type='tel'].has-information{border:1px solid #335280}input[type='text'].has-information:focus,input[type='date'].has-information:focus,input[type='datetime'].has-information:focus,input[type='datatime-local'].has-information:focus,input[type='month'].has-information:focus,input[type='time'].has-information:focus,input[type='week'].has-information:focus,input[type='color'].has-information:focus,input[type='number'].has-information:focus,input[type='search'].has-information:focus,input[type='password'].has-information:focus,input[type='email'].has-information:focus,input[type='url'].has-information:focus,input[type='tel'].has-information:focus{border:1px solid #335280}input[type='file']{outline:0;width:100%}input[type='reset']{display:none}input[type='search']{appearance:none;border-radius:0}input[type='search']::-webkit-search-results-decoration{display:none}input[type='search']::-webkit-search-cancel-button{-webkit-appearance:searchfield-cancel-button;cursor:pointer}input[type='checkbox'],input[type='radio']{float:left;height:1.5rem;margin-bottom:0;margin-right:1rem;outline:0;padding:0;vertical-align:middle;width:auto;min-height:1.5rem}input[type='checkbox'][disabled]+label,input[type='checkbox'][disabled='disabled']+label,input[type='radio'][disabled]+label,input[type='radio'][disabled='disabled']+label{cursor:not-allowed;opacity:.5}input[type='checkbox']+label,input[type='radio']+label{vertical-align:middle;width:100%}input[type='submit']{background-color:#0e8420;border:0;color:#fff;padding:.75rem 1.5rem}input[type='submit']:hover{background-color:#04280a;cursor:pointer}select{appearance:textfield;background-color:#fff;border:1px solid #cdcdcd;border-radius:.125rem;box-shadow:inset 0 1px 2px rgba(0,0,0,0.12);font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-weight:300;outline:0;padding:.8rem .5333rem;vertical-align:baseline;width:100%;appearance:none;background:#fff url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBoZWlnaHQ9IjRweCIgd2lkdGg9IjEwcHgiIHZlcnNpb249IjEuMSIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZpZXdCb3g9IjAgMCAxMCA0Ij4gPHRpdGxlPmFjY29yZGlvbi1vcGVuPC90aXRsZT4gPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+IDxnIGlkPSJmaWx0ZXItcGFuZWwiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSIgZmlsbD0ibm9uZSI+ICA8ZyBpZD0iYWNjb3JkaW9uLW9wZW4iIGZpbGw9IiM4ODgiIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiPiAgIDxwYXRoIGlkPSJjaGV2cm9uIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIiBkPSJtNi4zNjEgMC44NjIzYzAuNTE4IDAuMzY1IDEuMDUyIDAuNzc4MSAxLjYwMSAxLjIzOCAwLjU0OSAwLjQ1ODUgMS4wODkgMC45NTE4IDEuNjIxIDEuNDc3MiAwLjE0MiAwLjE0MDQgMC4yODEgMC4yODIxIDAuNDE1IDAuNDIyNWgtMS41NDFjLTAuMzA0LTAuMjg4OC0wLjYyLTAuNTcwOS0wLjk0Ny0wLjg0NjMtMC4xMzc5LTAuMTE2MS0wLjI3NjgtMC4yMjk3LTAuNDE2OC0wLjM0MDgtMC4xNjM2LTAuMTI5Ny0wLjMyODYtMC4yNTU4LTAuNDk1NC0wLjM3ODMtMC4wODUyLTAuMDYyNS0wLjE3MDgtMC4xMjQxLTAuMjU2OC0wLjE4NDYtMC4zOTctMC4yODIxLTAuOTM1LTAuNjI1Ny0xLjMxNS0wLjg0NzZoLTAuMDU0Yy0wLjM4IDAuMjIxOS0wLjkxOCAwLjU2NTUtMS4zMTUgMC44NDc2LTAuMzk4IDAuMjgwNy0wLjc4OCAwLjU4MjktMS4xNjkgMC45MDM3LTAuMzI3IDAuMjc1NC0wLjY0MyAwLjU1NzUtMC45NDcgMC44NDYzaC0xLjU0MWMwLjEzNS0wLjE0MDQgMC4yNzMtMC4yODIxIDAuNDE1LTAuNDIyNSAwLjUzMi0wLjUyNTQgMS4wNzItMS4wMTg3IDEuNjIxLTEuNDc3MiAwLjU1LTAuNDU5OSAxLjA4My0wLjg3MyAxLjYwMS0xLjIzOCAwLjUxOS0wLjM2NDk3IDAuOTczLTAuNjUyNDEgMS4zNjItMC44NjIzIDAuMzkgMC4yMDk4OSAwLjg0NCAwLjQ5NzMzIDEuMzYyIDAuODYyM3oiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQuOTk5IDIpIHJvdGF0ZSgxODApIHRyYW5zbGF0ZSgtNC45OTkgLTIpIi8+ICA8L2c+IDwvZz48L3N2Zz4=") no-repeat;background-position:top 1.3rem right 1.25rem;line-height:1rem;margin-top:.5rem;min-height:48px;text-indent:.01px;text-overflow:''}@media(min-width:768px){select{padding:.75rem .5rem}}select:active,select:focus{border-color:#666;color:#111;outline:0}select::placeholder{color:#666;opacity:1}select[disabled],select[disabled='disabled']{cursor:not-allowed;opacity:.5}select[readonly],select[readonly='readonly']{color:#cdcdcd;cursor:default}select[readonly]:hover,select[readonly]:active,select[readonly]:focus,select[readonly='readonly']:hover,select[readonly='readonly']:active,select[readonly='readonly']:focus{border-color:#666;outline:0}select.has-error{border:1px solid #c7162b}select.has-error:focus{border:1px solid #c7162b}select.has-caution{border:1px solid #f99b11}select.has-caution:focus{border:1px solid #f99b11}select.has-warning{border:1px solid #f99b11}select.has-warning:focus{border:1px solid #f99b11}select.has-success{border:1px solid #0e8420}select.has-success:focus{border:1px solid #0e8420}select.has-information{border:1px solid #335280}select.has-information:focus{border:1px solid #335280}select:hover{cursor:pointer}select[multiple],select[size]{background-image:none;height:auto;padding:.35rem .8125rem}select[multiple] option,select[size] option{font-weight:300;margin:.5rem 0}textarea{appearance:textfield;background-color:#fff;border:1px solid #cdcdcd;border-radius:.125rem;box-shadow:inset 0 1px 2px rgba(0,0,0,0.12);font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-weight:300;outline:0;padding:.8rem .5333rem;vertical-align:baseline;width:100%;margin-top:.5rem;overflow:auto;vertical-align:top}@media(min-width:768px){textarea{padding:.75rem .5rem}}textarea:active,textarea:focus{border-color:#666;color:#111;outline:0}textarea::placeholder{color:#666;opacity:1}textarea[disabled],textarea[disabled='disabled']{cursor:not-allowed;opacity:.5}textarea[readonly],textarea[readonly='readonly']{color:#cdcdcd;cursor:default}textarea[readonly]:hover,textarea[readonly]:active,textarea[readonly]:focus,textarea[readonly='readonly']:hover,textarea[readonly='readonly']:active,textarea[readonly='readonly']:focus{border-color:#666;outline:0}textarea.has-error{border:1px solid #c7162b}textarea.has-error:focus{border:1px solid #c7162b}textarea.has-caution{border:1px solid #f99b11}textarea.has-caution:focus{border:1px solid #f99b11}textarea.has-warning{border:1px solid #f99b11}textarea.has-warning:focus{border:1px solid #f99b11}textarea.has-success{border:1px solid #0e8420}textarea.has-success:focus{border:1px solid #0e8420}textarea.has-information{border:1px solid #335280}textarea.has-information:focus{border:1px solid #335280}fieldset{background-color:#f7f7f7;background-position:-.9375rem -.9375rem;background-repeat:no-repeat;border-radius:.125rem;padding:.9375rem 1.25rem}@media only screen and (min-width:1030px){fieldset{padding:.9375rem 1.25rem}}fieldset h3{border-bottom:1px dotted #666;padding-bottom:.625rem}form+*{margin-top:.5rem}form *+input{margin-top:.75rem}form *+label{margin-top:1.25rem}form *+input[type="checkbox"],form *+input[type="radio"],form *+button,form *+input[type="submit"]{margin-top:1rem}form *+input[type="checkbox"]+label,form *+input[type="radio"]+label,form *+button+label,form *+input[type="submit"]+label{margin-top:1rem}code,samp,kbd{font-family:"Ubuntu Mono",Consolas,Monaco,Courier,monospace;font-weight:300;text-align:left}pre,code{direction:ltr;hyphens:none;line-height:1.5;margin-bottom:0;tab-size:4;white-space:pre-wrap;word-spacing:normal;word-wrap:break-word}pre{background-color:#f7f7f7;border:1px solid #666;border-radius:2px;color:#111;overflow:auto;padding:1rem;text-align:left;text-shadow:none}a{color:#007aa6;text-decoration:none}a:focus{outline:thin dotted #cdcdcd}a:hover{cursor:pointer;text-decoration:underline}a:visited{color:#005573}ol,ul{margin-bottom:0;margin-left:1rem;padding-left:1rem}ol ul,ol ol,ul ul,ul ol{margin-bottom:0}nav ol,nav ul{list-style:none;list-style-image:none}ol li+li,ul li+li{margin-top:.5rem}ol li>ul,ol li>ol,ul li>ul,ul li>ol{margin-top:.5rem}li{margin:0 0 .5rem;padding:0}dl{margin-bottom:0}dt{border-top:1px dotted #666;font-size:1rem;font-weight:400;margin-top:1rem;padding-top:1rem}dt:first-of-type{border-top:0}dd{margin-left:20px;margin-top:.5rem}.p-heading--one{font-size:2rem;font-weight:300;line-height:1.2}@media only screen and (min-width:1030px){.p-heading--one{font-size:3rem;line-height:1.25}}.p-heading--two{font-size:1.75rem;font-weight:300;line-height:1.25}@media only screen and (min-width:1030px){.p-heading--two{font-size:2rem;line-height:1.25}}@media only screen and (min-width:1030px){.p-heading--two{font-size:2.25rem;line-height:1.167}}.p-heading--three{font-size:1.5rem;font-weight:300;line-height:1.154}@media only screen and (min-width:1030px){.p-heading--three{font-size:1.75rem;line-height:1.286}}.p-heading--four{font-size:1.375rem;font-weight:300;line-height:1.364}@media only screen and (min-width:1030px){.p-heading--four{font-size:1.5rem;line-height:1.25}}.p-heading--five{font-size:1.125rem;font-weight:300;line-height:1.264}@media only screen and (min-width:1030px){.p-heading--five{font-size:1.25rem;line-height:1.143}}.p-heading--six{font-size:1rem;font-weight:400;line-height:1.412}hr{border:0;border-top:1px solid #cdcdcd;height:0}img{border:0;height:auto;max-width:100%}svg:not(:root){overflow:hidden}figure{margin-bottom:0;margin-left:0;width:100%}*+figure{margin-top:1.25rem}@media screen and (min-width:768px){*+figure{margin-top:1.75rem}}@media screen and (min-width:1030px){*+figure{margin-top:2rem}}figure caption,figure figcaption{display:block;font-style:italic;margin-top:.5rem;width:100%}object,iframe,embed,canvas,video,audio{display:block;margin:0 auto 20px;max-width:100%}audio:not([controls]){display:none;height:0}[hidden]{display:none}table{border:0;border-collapse:collapse;overflow-x:auto;width:100%}th,td{padding:1rem .75rem}td{font-weight:300;text-align:left;vertical-align:middle}thead th{border-collapse:separate;border-spacing:0 .5rem;font-weight:400;text-align:left}thead tr{border-bottom:1px solid #666}tbody tr{border-bottom:1px solid #cdcdcd}tbody th{font-weight:400;text-align:left}@font-face{font-family:'Ubuntu';font-style:normal;font-weight:300;src:url("https://assets.ubuntu.com/v1/50afa266-ubuntu-l-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/0194407b-ubuntu-l-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:normal;font-weight:400;src:url("https://assets.ubuntu.com/v1/1cbafee5-ubuntu-r-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/81863185-ubuntu-r-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:italic;font-weight:300;src:url("https://assets.ubuntu.com/v1/abb07502-ubuntu-li-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/65fc9630-ubuntu-li-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:italic;font-weight:400;src:url("https://assets.ubuntu.com/v1/fca66073-ubuntu-ri-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/f0898c72-ubuntu-ri-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu Mono';font-style:normal;font-weight:300;src:url("https://assets.ubuntu.com/v1/871f7456-ubuntumono-r-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/8df3f408-ubuntumono-r-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu Mono';font-style:normal;font-weight:400;src:url("https://assets.ubuntu.com/v1/871f7456-ubuntumono-r-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/8df3f408-ubuntumono-r-webfont.woff") format("woff")}*{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-smoothing:subpixel-antialiased}html{font-size:16px}body{color:#111;font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-weight:300;line-height:1.5}h1,h2,h3,h4,h5,h6,[class^="p-heading--"]{font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;margin:0}h1+*{margin-top:1rem}@media screen and (min-width:1030px){h1+*{margin-top:1.5rem}}h5+*,h6+*{margin-top:.5rem}@media screen and (min-width:1030px){h5+*,h6+*{margin-top:.75rem}}p{margin-bottom:0}p+p{margin-top:1rem}@media screen and (min-width:1030px){p+p{margin-top:1.5rem}}button,input,select,textarea{font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif}h1{font-size:2rem;font-weight:300;line-height:1.2}@media only screen and (min-width:1030px){h1{font-size:3rem;line-height:1.25}}h2{font-size:1.75rem;font-weight:300;line-height:1.25}@media only screen and (min-width:1030px){h2{font-size:2rem;line-height:1.25}}@media only screen and (min-width:1030px){h2{font-size:2.25rem;line-height:1.167}}h3{font-size:1.5rem;font-weight:300;line-height:1.154}@media only screen and (min-width:1030px){h3{font-size:1.75rem;line-height:1.286}}h4{font-size:1.375rem;font-weight:300;line-height:1.364}@media only screen and (min-width:1030px){h4{font-size:1.5rem;line-height:1.25}}h5{font-size:1.125rem;font-weight:300;line-height:1.264}@media only screen and (min-width:1030px){h5{font-size:1.25rem;line-height:1.143}}h6{font-size:1rem;font-weight:400;line-height:1.412}li{margin-bottom:0;margin-top:0;padding-bottom:0}li>ul,li>ol{padding-top:0}li>ul>li:last-of-type,li>ol>li:last-of-type{padding-bottom:0}blockquote{margin-bottom:0;margin-left:0;padding-left:1.5rem}blockquote>p{font-size:1rem;font-style:italic;margin-top:.75rem}blockquote>cite{font-size:1rem;font-style:normal;margin-top:.75rem}strong{font-weight:400}small{font-size:.8125rem}sub,sup{font-size:.75rem;line-height:0;position:relative;vertical-align:baseline}sup{vertical-align:text-top}sub{vertical-align:text-bottom}.p-breadcrumbs{list-style:none;margin:0;padding:0;width:100%}.p-breadcrumbs__item{display:inline-block;margin:0 .75rem .25rem .25rem;position:relative}.p-breadcrumbs__item:not(:first-of-type){margin-right:-.25rem;text-indent:1rem}.p-breadcrumbs__item:first-of-type{margin-right:-.25rem}.p-breadcrumbs__item:not(:first-of-type)::before{content:'\203A';left:-.75rem;position:absolute;top:0}.p-breadcrumbs__link{color:#111;font-weight:400;text-decoration:none}.p-breadcrumbs__link:hover{color:#007aa6}.p-button{transition-duration:.165s;transition-property:background-color;transition-timing-function:cubic-bezier(0.55,0.055,0.675,0.19);background-color:#fff;border-color:#cdcdcd;border-radius:.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#111;cursor:pointer;display:inline-block;font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-smoothing:subpixel-antialiased;font-weight:300;line-height:1rem;outline:0;padding:.75rem 1.5rem;text-align:center;text-decoration:none;width:100%}@media only screen and (min-width:768px){.p-button{width:auto}}.p-button:visited{color:#111}.p-button:active,.p-button:focus,.p-button:hover{background-color:#f7f7f7;border-color:#cdcdcd;text-decoration:none}.p-button:disabled,.p-button.is--disabled{cursor:not-allowed;opacity:.5}.p-button:disabled:active,.p-button:disabled:focus,.p-button:disabled:hover,.p-button.is--disabled:active,.p-button.is--disabled:focus,.p-button.is--disabled:hover{background-color:#fff;border-color:#fff}.p-button .p-link--external{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-button--neutral{transition-duration:.165s;transition-property:background-color;transition-timing-function:cubic-bezier(0.55,0.055,0.675,0.19);background-color:#fff;border-color:#cdcdcd;border-radius:.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#111;cursor:pointer;display:inline-block;font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-smoothing:subpixel-antialiased;font-weight:300;line-height:1rem;outline:0;padding:.75rem 1.5rem;text-align:center;text-decoration:none;width:100%}@media only screen and (min-width:768px){.p-button--neutral{width:auto}}.p-button--neutral:visited{color:#111}.p-button--neutral:active,.p-button--neutral:focus,.p-button--neutral:hover{background-color:#f7f7f7;border-color:#cdcdcd;text-decoration:none}.p-button--neutral:disabled,.p-button--neutral.is--disabled{cursor:not-allowed;opacity:.5}.p-button--neutral:disabled:active,.p-button--neutral:disabled:focus,.p-button--neutral:disabled:hover,.p-button--neutral.is--disabled:active,.p-button--neutral.is--disabled:focus,.p-button--neutral.is--disabled:hover{background-color:transparent;border-color:#cdcdcd}.p-button--neutral .p-link--external{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-button--brand{transition-duration:.165s;transition-property:background-color;transition-timing-function:cubic-bezier(0.55,0.055,0.675,0.19);background-color:#333;border-color:#333;border-radius:.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#fff;cursor:pointer;display:inline-block;font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-smoothing:subpixel-antialiased;font-weight:300;line-height:1rem;outline:0;padding:.75rem 1.5rem;text-align:center;text-decoration:none;width:100%}@media only screen and (min-width:768px){.p-button--brand{width:auto}}.p-button--brand:visited{color:#fff}.p-button--brand:active,.p-button--brand:focus,.p-button--brand:hover{background-color:#1a1a1a;border-color:#1a1a1a;text-decoration:none}.p-button--brand:disabled,.p-button--brand.is--disabled{cursor:not-allowed;opacity:.5}.p-button--brand:disabled:active,.p-button--brand:disabled:focus,.p-button--brand:disabled:hover,.p-button--brand.is--disabled:active,.p-button--brand.is--disabled:focus,.p-button--brand.is--disabled:hover{background-color:#333;border-color:#333}.p-button--brand .p-link--external{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23fff' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-button--positive{transition-duration:.165s;transition-property:background-color;transition-timing-function:cubic-bezier(0.55,0.055,0.675,0.19);background-color:#0e8420;border-color:#0e8420;border-radius:.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#fff;cursor:pointer;display:inline-block;font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-smoothing:subpixel-antialiased;font-weight:300;line-height:1rem;outline:0;padding:.75rem 1.5rem;text-align:center;text-decoration:none;width:100%}@media only screen and (min-width:768px){.p-button--positive{width:auto}}.p-button--positive:visited{color:#fff}.p-button--positive:active,.p-button--positive:focus,.p-button--positive:hover{background-color:#095615;border-color:#095615;text-decoration:none}.p-button--positive:disabled,.p-button--positive.is--disabled{cursor:not-allowed;opacity:.5}.p-button--positive:disabled:active,.p-button--positive:disabled:focus,.p-button--positive:disabled:hover,.p-button--positive.is--disabled:active,.p-button--positive.is--disabled:focus,.p-button--positive.is--disabled:hover{background-color:#0e8420;border-color:#0e8420}.p-button--positive .p-link--external{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23fff' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-button--negative{transition-duration:.165s;transition-property:background-color;transition-timing-function:cubic-bezier(0.55,0.055,0.675,0.19);background-color:#c7162b;border-color:#c7162b;border-radius:.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#fff;cursor:pointer;display:inline-block;font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-smoothing:subpixel-antialiased;font-weight:300;line-height:1rem;outline:0;padding:.75rem 1.5rem;text-align:center;text-decoration:none;width:100%}@media only screen and (min-width:768px){.p-button--negative{width:auto}}.p-button--negative:visited{color:#fff}.p-button--negative:active,.p-button--negative:focus,.p-button--negative:hover{background-color:#991121;border-color:#991121;text-decoration:none}.p-button--negative:disabled,.p-button--negative.is--disabled{cursor:not-allowed;opacity:.5}.p-button--negative:disabled:active,.p-button--negative:disabled:focus,.p-button--negative:disabled:hover,.p-button--negative.is--disabled:active,.p-button--negative.is--disabled:focus,.p-button--negative.is--disabled:hover{background-color:#c7162b;border-color:#c7162b}.p-button--negative .p-link--external{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23fff' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-button--base{transition-duration:.165s;transition-property:background-color;transition-timing-function:cubic-bezier(0.55,0.055,0.675,0.19);background-color:transparent;border-color:transparent;border-radius:.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#111;cursor:pointer;display:inline-block;font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-smoothing:subpixel-antialiased;font-weight:300;line-height:1rem;outline:0;padding:.75rem 1.5rem;text-align:center;text-decoration:none;width:100%}@media only screen and (min-width:768px){.p-button--base{width:auto}}.p-button--base:visited{color:#111}.p-button--base:active,.p-button--base:focus,.p-button--base:hover{background-color:#f7f7f7;border-color:transparent;text-decoration:none}.p-button--base:disabled,.p-button--base.is--disabled{cursor:not-allowed;opacity:.5}.p-button--base:disabled:active,.p-button--base:disabled:focus,.p-button--base:disabled:hover,.p-button--base.is--disabled:active,.p-button--base.is--disabled:focus,.p-button--base.is--disabled:hover{background-color:transparent;border-color:#cdcdcd}.p-button--base .p-link--external{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}@media(max-width:768px){[class^="p-button"].is-inline{margin-top:1.2rem}}@media(min-width:768px){[class^="p-button"].is-inline{margin-left:.75rem;width:auto}}.p-card{background:#fff;border-radius:2px;padding:1.25rem;border:1px solid #cdcdcd}.p-card__title{margin:0 0 .5rem}.p-card__content{margin-bottom:1rem;margin-top:0}.p-card__footer{border-top:1px solid #cdcdcd;margin-top:0;padding-top:1rem}.p-card__header{border-bottom:1px solid #cdcdcd;font-size:.75rem;margin-bottom:.75rem;padding-bottom:.75rem}.p-card__header>img{max-height:2rem}.p-card .p-card{margin-top:0}.p-card--highlighted{background:#fff;border-radius:2px;padding:1.25rem;box-shadow:0 1px 5px 1px rgba(17,17,17,0.2)}.p-card--highlighted__title{margin:0 0 .5rem}.p-card--highlighted__content{margin-bottom:1rem;margin-top:0}.p-card--highlighted__footer{border-top:1px solid #cdcdcd;margin-top:0;padding-top:1rem}.p-card--highlighted__header{border-bottom:1px solid #cdcdcd;font-size:.75rem;margin-bottom:.75rem;padding-bottom:.75rem}.p-card--highlighted__header>img{max-height:2rem}.p-code-numbered{background:#fff;color:#111;counter-reset:line-numbering;padding:1rem 0 0;position:relative}.p-code-numbered::before{background-color:#fff;width:4.5rem}.p-code-numbered .code-line{background:#f7f7f7;display:block;margin:-1.5rem 0 0 0;padding:.5rem 1rem 0 5.5rem;position:relative}.p-code-numbered .code-line:first-child,.p-code-numbered .code-line:first-child::before{padding-top:1.25rem}.p-code-numbered .code-line:last-child,.p-code-numbered .code-line:last-child::before{padding-bottom:1rem}.p-code-numbered .code-line::before{background:#fff;border-right:1px solid #111;color:#666;content:counter(line-numbering);counter-increment:line-numbering;display:inline-block;height:9999px;left:0;margin-right:1rem;max-height:100%;padding:.5rem 1rem 1rem 1rem;pointer-events:none;position:absolute;text-align:right;top:0;user-select:none;width:4.5rem}.p-code-snippet{background-color:#fff;border:1px solid #666;border-radius:2px;display:flex;overflow:hidden;position:relative;transition:border .2s,background-color .2s;width:100%}.p-code-snippet__input{background-color:transparent;background-image:url('data:image/svg+xml;utf8, <svg xmlns="http://www.w3.org/2000/svg" width="16" height="15.999999" viewBox="0 0 16 15.999999"><g><g style="display:inline"><g style="display:inline"><path style="opacity:0.21171169;fill:none;stroke:none" d="M-.0000032.00002047h15.9999936v15.9999936H-.0000032z"/><path style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none" d="M2.6660124 2.00000047c-1.77777926 0-2.6660156.0013069-2.6660156 2.0683594v8.8652346c0 2.067046.88823634 2.066406 2.6660156 2.066406h10.6679684c1.77778 0 2.666016.00064 2.666016-2.066406v-8.7988284c0-2.1333325-.888236-2.1347656-2.666016-2.1347656H2.6660124zm1.2792969 1.890625h1.1015625v1.1425781c.3388576.0282222.6418942.0778287.9101562.1484375.2682622.0635378.4794546.127873.6347657.1914063l-.2636719 1.046875c-.2047288-.0776578-.4480911-.1520607-.7304688-.2226563-.2753242-.0705955-.5930895-.1054687-.953125-.1054687-.381213 0-.6687661.0716995-.859375.2128906-.1906042.1341333-.2851562.3205247-.2851562.5605469 0 .141191.0275088.2605439.0839844.359375.0564755.0917777.1429083.1762529.2558594.2539062.1129509.0705956.2497361.1422952.4121093.2128906.1623688.0635334.3460569.1305764.5507813.2011719.2894399.1129555.560311.232304.8144531.359375.2612043.1200089.4871256.2661159.6777344.4355469.1906043.1623688.3394192.3561248.4453125.5820312.112951.2259022.1699219.4940697.1699218.8046878 0 .465928-.1441538.868173-.4335937 1.207031s-.7660922.557414-1.4296875.65625v1.324219H3.9453093v-1.292969c-.5082842-.035289-.9225545-.102332-1.2402344-.201172-.3106176-.105893-.5419546-.200441-.6972656-.285156l.359375-1.00586c.2259066.112956.4967733.214868.8144531.306641.3247377.091773.6921094.138672 1.1015625.138672.4871065 0 .8223128-.0717 1.0058594-.212891.1906088-.148248.2871094-.342004.2871094-.582031 0-.1623686-.0395298-.3038192-.1171875-.423828-.0776533-.1200133-.186934-.2265861-.328125-.3183594-.1411911-.0917733-.3101459-.1762485-.5078125-.2539062-.1906044-.0776533-.4037544-.157472-.6367188-.2421875-.2188488-.0776533-.4374056-.1667895-.65625-.265625-.2117866-.0988311-.4055469-.218184-.5820312-.359375-.1694311-.1482489-.3062161-.3245681-.4121094-.5292969-.1058933-.2047244-.1601563-.455451-.1601563-.7519531e-7-.4871065.146107-.9013768.4355469-1.2402344.2894444-.3459154.7339269-.5671801 1.3339844-.6660156v-1.1855469zm4.0546875 8.095703h3.990234v.996094h-3.990234v-.996094z"/></g></g></g></svg>');background-position:8px center;background-repeat:no-repeat;border:0;box-shadow:inset 0 1px 2px 0 rgba(0,0,0,0.12);color:#666;font-family:"Ubuntu Mono",Consolas,Monaco,Courier,monospace;font-size:1em;font-weight:300;padding:8px 8px 8px 32px;width:100%}.p-code-snippet__action{background-color:#f7f7f7;background-image:url('data:image/svg+xml;utf8, <svg xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 16 15.999999" width="16"><g><g><g color="%23000"><path fill="none" enable-background="accumulate" d="M.174.126h16.008v16.008H.174z"/><path fill="none" enable-background="accumulate" d="M.174.126h16.008v16.008H.174z"/><path d="M5.023 11.285L11.33 4.98" stroke="%23808080" stroke-width="1.334.194" fill="none" enable-background="accumulate"/><path style="text-decoration-color:%23000000;isolation:auto;block-progression:tb;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none" d="M12.333.126c-.11.003-.22.017-.33.038-.868.174-1.42.782-1.9 1.262L8.667 2.864c-.48.48-1.087 1.03-1.26 1.9-.075.37-.044.76.096 1.166l1.77-1.77c.103-.113.216-.23.338-.352l1.438-1.438c.48-.48.887-.83 1.217-.897.042-.008.085-.014.13-.016.047-.002.096 0 .147.005.31.037.737.248 1.397.908.88.88.96 1.343.895 1.673-.067.33-.417.738-.897 1.217L12.5 6.698c-.123.123-.24.237-.354.34l-1.768 1.77c.406.14.797.17 1.168.095.868-.173 1.42-.782 1.9-1.26l1.437-1.44c.48-.478 1.087-1.03 1.26-1.898.174-.868-.223-1.843-1.26-2.88-.778-.778-1.52-1.196-2.21-1.283-.115-.014-.228-.02-.34-.016zm-7.19 7.19c-.112.004-.222.017-.33.04-.868.172-1.42.78-1.9 1.26l-1.437 1.438c-.48.48-1.087 1.03-1.26 1.9-.175.867.222 1.84 1.26 2.88 1.037 1.036 2.012 1.433 2.88 1.26.867-.174 1.42-.782 1.898-1.262l1.44-1.438c.478-.48 1.086-1.03 1.26-1.9.074-.37.043-.76-.097-1.167L7.09 12.093c-.103.114-.217.233-.34.357l-1.44 1.438c-.48.48-.886.83-1.217.896-.33.066-.793-.016-1.673-.896s-.962-1.343-.896-1.673c.066-.33.417-.738.896-1.217L3.858 9.56c.123-.123.24-.237.354-.34l1.77-1.77c-.17-.056-.336-.097-.5-.118-.114-.014-.227-.02-.34-.016z" fill="%23808080" enable-background="accumulate"/></g></g></g></svg>');background-position:center;background-repeat:no-repeat;background-size:1rem;border-color:transparent;border-left:1px solid #666;border-radius:0;display:block;flex:1;height:100%;padding:0;position:absolute;right:0;text-indent:-9999px;top:0;width:40px}.p-code-snippet__action:hover{border-color:transparent;border-left:1px solid #666}.p-footer{border-top:1px dotted #000;font-size:.75rem;margin-bottom:1rem;padding-top:.75rem}@media(min-width:768px){.p-footer{font-size:.8125rem}}.p-footer__copy{margin-bottom:0}.p-footer__links{margin:0;padding:.5rem 0}@media(min-width:768px){.p-footer__links{margin-top:0;padding:0}}.p-footer__item{display:block;margin-bottom:.25rem}@media(min-width:768px){.p-footer__item{display:inline-block}}.p-footer__item:last-child a::after{opacity:0}.p-footer__link{border-bottom:0;color:#111;font-size:.75rem}.p-footer__link:visited{color:black}.p-footer__link:hover{color:#007aa6}@media(min-width:768px){.p-footer__link{font-size:.8125rem;margin-right:1rem}.p-footer__link::after{content:'\00b7';display:inline-block;font-size:1.5rem;left:.5rem;position:relative;top:.2rem}}.p-footer__link:hover::after{color:#111}.p-matrix{list-style:none;margin:0;padding:0}@media(min-width:620px){.p-matrix{display:flex;flex-wrap:wrap}}.p-matrix__item{border-top:1px dotted #666;margin-top:0;padding:1rem}.p-matrix__item:empty{display:none}.p-matrix__item:first-child{border-top:0}@media(min-width:620px){.p-matrix__item{border-right:1px dotted #666;border-top:1px dotted #666;margin-bottom:0;padding-right:1rem;width:calc(33.333% - .666666rem)}.p-matrix__item:empty{display:block}.p-matrix__item:nth-child(-n+3){border-top:0}.p-matrix__item:nth-child(2n){border-right:1px dotted #666;padding-right:1rem}.p-matrix__item:nth-child(3n){border-right:0;padding-right:0}}.p-matrix__img,.p-matrix__content{display:inline-block;float:left;margin-top:0}.p-matrix__img{margin-right:1rem;max-width:calc(30% - 1rem)}.p-matrix__content{max-width:70%}.p-matrix__title{margin-bottom:.5rem;margin-top:0}.p-matrix__desc{margin-top:0}.p-matrix__link{border-top:0}.p-navigation--light{background-color:#f7f7f7;color:#111;position:relative;width:100%}.p-navigation--light .row{padding:0}@media(max-width:768px){.p-navigation--light .p-navigation__banner{overflow:hidden;position:relative}}.p-navigation--light .p-navigation__toggle--open,.p-navigation--light .p-navigation__toggle--close,.p-navigation--light .p-navigation__link{color:#111}.p-navigation--light .p-navigation__toggle--open:hover,.p-navigation--light .p-navigation__toggle--close:hover,.p-navigation--light .p-navigation__link:hover{border-bottom:0;text-decoration:underline}.p-navigation--light .p-navigation__toggle--open:visited,.p-navigation--light .p-navigation__toggle--close:visited,.p-navigation--light .p-navigation__link:visited{color:#111}.p-navigation--light .p-navigation__toggle--close{display:none}.p-navigation--light .p-navigation__toggle--open,.p-navigation--light .p-navigation__toggle--close{margin:0;position:absolute;right:1rem;top:calc(50% - .75rem)}@media(min-width:769px){.p-navigation--light .p-navigation__toggle--open,.p-navigation--light .p-navigation__toggle--close{display:none}}.p-navigation--light .p-navigation__logo{font-size:1.375rem;font-weight:300;line-height:1.364;align-items:center;display:flex;float:left;margin:.75rem .5rem}@media only screen and (min-width:1030px){.p-navigation--light .p-navigation__logo{font-size:1.5rem;line-height:1.25}}@media(min-width:769px){.p-navigation--light .p-navigation__logo{margin:.5rem 1.25rem}}.p-navigation--light .p-navigation__link{border-bottom:0;display:block;margin-top:0}@media(min-width:769px){.p-navigation--light .p-navigation__link{display:block;float:left;width:auto}}.p-navigation--light .p-navigation__link>a{border-bottom:0;color:#111;font-size:.93333rem}@media(min-width:769px){.p-navigation--light .p-navigation__link>a{color:#111;font-size:.875rem}}.p-navigation--light .p-navigation__link:last-child{margin-bottom:0}.p-navigation--light .p-navigation__links{background-color:#cdcdcd;clear:both;margin:0;padding:0}@media(min-width:769px){.p-navigation--light .p-navigation__links{background-color:transparent;clear:none;float:left}}.p-navigation--light .p-navigation__links .p-navigation__link{border-left:1px solid #cdcdcd;padding:.75rem .5rem}@media(max-width:768px){.p-navigation--light .p-navigation__links .p-navigation__link{background-color:#f7f7f7;border-left:0;border-top:1px solid #cdcdcd;color:#111;text-align:left}.p-navigation--light .p-navigation__links .p-navigation__link:last-child{border-bottom:1px solid #cdcdcd}}@media(min-width:769px){.p-navigation--light .p-navigation__links .p-navigation__link{padding:.75rem 1.25rem}}@media(max-width:768px){.p-navigation--light .p-navigation__links>a{color:#111;display:block}}.p-navigation--light .p-navigation__links:last-of-type{border-right:1px solid #cdcdcd}@media(max-width:768px){.p-navigation--light .p-navigation__links:last-of-type{border-bottom:0;border-right:0}}.p-navigation--light .p-navigation__nav{display:none;margin-top:0}@media(min-width:769px){.p-navigation--light .p-navigation__nav{display:block}}.p-navigation--light:target .p-navigation__toggle--open{display:none}@media(max-width:768px){.p-navigation--light:target .p-navigation__toggle--close{display:inline-block}}.p-navigation--light:target .p-navigation__nav{display:block}.p-navigation,.p-navigation--dark{background-color:#111;color:#fff;position:relative;width:100%}.p-navigation .row,.p-navigation--dark .row{padding:0}@media(max-width:768px){.p-navigation .p-navigation__banner,.p-navigation--dark .p-navigation__banner{overflow:hidden;position:relative}}.p-navigation .p-navigation__toggle--open,.p-navigation .p-navigation__toggle--close,.p-navigation .p-navigation__link,.p-navigation--dark .p-navigation__toggle--open,.p-navigation--dark .p-navigation__toggle--close,.p-navigation--dark .p-navigation__link{color:#fff}.p-navigation .p-navigation__toggle--open:hover,.p-navigation .p-navigation__toggle--close:hover,.p-navigation .p-navigation__link:hover,.p-navigation--dark .p-navigation__toggle--open:hover,.p-navigation--dark .p-navigation__toggle--close:hover,.p-navigation--dark .p-navigation__link:hover{border-bottom:0;text-decoration:underline}.p-navigation .p-navigation__toggle--open:visited,.p-navigation .p-navigation__toggle--close:visited,.p-navigation .p-navigation__link:visited,.p-navigation--dark .p-navigation__toggle--open:visited,.p-navigation--dark .p-navigation__toggle--close:visited,.p-navigation--dark .p-navigation__link:visited{color:#fff}.p-navigation .p-navigation__toggle--close,.p-navigation--dark .p-navigation__toggle--close{display:none}.p-navigation .p-navigation__toggle--open,.p-navigation .p-navigation__toggle--close,.p-navigation--dark .p-navigation__toggle--open,.p-navigation--dark .p-navigation__toggle--close{margin:0;position:absolute;right:1rem;top:calc(50% - .75rem)}@media(min-width:769px){.p-navigation .p-navigation__toggle--open,.p-navigation .p-navigation__toggle--close,.p-navigation--dark .p-navigation__toggle--open,.p-navigation--dark .p-navigation__toggle--close{display:none}}.p-navigation .p-navigation__logo,.p-navigation--dark .p-navigation__logo{font-size:1.375rem;font-weight:300;line-height:1.364;align-items:center;display:flex;float:left;margin:.75rem .5rem}@media only screen and (min-width:1030px){.p-navigation .p-navigation__logo,.p-navigation--dark .p-navigation__logo{font-size:1.5rem;line-height:1.25}}@media(min-width:769px){.p-navigation .p-navigation__logo,.p-navigation--dark .p-navigation__logo{margin:.5rem 1.25rem}}.p-navigation .p-navigation__link,.p-navigation--dark .p-navigation__link{border-bottom:0;display:block;margin-top:0}@media(min-width:769px){.p-navigation .p-navigation__link,.p-navigation--dark .p-navigation__link{display:block;float:left;width:auto}}.p-navigation .p-navigation__link>a,.p-navigation--dark .p-navigation__link>a{border-bottom:0;color:#111;font-size:.93333rem}@media(min-width:769px){.p-navigation .p-navigation__link>a,.p-navigation--dark .p-navigation__link>a{color:#fff;font-size:.875rem}}.p-navigation .p-navigation__link:last-child,.p-navigation--dark .p-navigation__link:last-child{margin-bottom:0}.p-navigation .p-navigation__links,.p-navigation--dark .p-navigation__links{background-color:#cdcdcd;clear:both;margin:0;padding:0}@media(min-width:769px){.p-navigation .p-navigation__links,.p-navigation--dark .p-navigation__links{background-color:transparent;clear:none;float:left}}.p-navigation .p-navigation__links .p-navigation__link,.p-navigation--dark .p-navigation__links .p-navigation__link{border-left:1px solid #cdcdcd;padding:.75rem .5rem}@media(max-width:768px){.p-navigation .p-navigation__links .p-navigation__link,.p-navigation--dark .p-navigation__links .p-navigation__link{background-color:#f7f7f7;border-left:0;border-top:1px solid #cdcdcd;color:#111;text-align:left}.p-navigation .p-navigation__links .p-navigation__link:last-child,.p-navigation--dark .p-navigation__links .p-navigation__link:last-child{border-bottom:1px solid #cdcdcd}}@media(min-width:769px){.p-navigation .p-navigation__links .p-navigation__link,.p-navigation--dark .p-navigation__links .p-navigation__link{padding:.75rem 1.25rem}}@media(max-width:768px){.p-navigation .p-navigation__links>a,.p-navigation--dark .p-navigation__links>a{color:#111;display:block}}.p-navigation .p-navigation__links:last-of-type,.p-navigation--dark .p-navigation__links:last-of-type{border-right:1px solid #cdcdcd}@media(max-width:768px){.p-navigation .p-navigation__links:last-of-type,.p-navigation--dark .p-navigation__links:last-of-type{border-bottom:0;border-right:0}}.p-navigation .p-navigation__nav,.p-navigation--dark .p-navigation__nav{display:none;margin-top:0}@media(min-width:769px){.p-navigation .p-navigation__nav,.p-navigation--dark .p-navigation__nav{display:block}}.p-navigation:target .p-navigation__toggle--open,.p-navigation--dark:target .p-navigation__toggle--open{display:none}@media(max-width:768px){.p-navigation:target .p-navigation__toggle--close,.p-navigation--dark:target .p-navigation__toggle--close{display:inline-block}}.p-navigation:target .p-navigation__nav,.p-navigation--dark:target .p-navigation__nav{display:block}.p-link--external{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23007aa6' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23007aa6' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23007aa6' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E");background-position:100% top;background-repeat:no-repeat;background-size:.7rem .7rem;padding-right:.9rem}.p-link--no-underline{border:0}.p-link--soft{color:#111}.p-link--soft:visited{color:#111;text-decoration:none}.p-link--soft:hover{color:#007aa6}.p-link--soft.is-selected{font-weight:400}.p-link--strong{color:#111;font-weight:400}.p-link--strong:visited{color:#111}.p-link--strong:hover{color:#007aa6;text-decoration:underline}.p-link--strong.p-link--external:hover::after{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-link--inverted{color:#f7f7f7;font-weight:400}.p-link--inverted:hover{color:#f7f7f7}.p-link--inverted:visited{color:#dedede}.p-top{border-bottom:1px dotted #cdcdcd;clear:both;margin:20px 0}.p-top__link{background:#fff;float:right;margin-right:5px;padding:0 5px;position:relative;text-decoration:none;top:-.725rem}.p-link--external.p-link--strong{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E");color:#111}.p-list{list-style:none;margin-left:0;padding-left:0}.p-list__item{margin-top:.6667rem}.p-list--divided{list-style:none;margin-left:0;padding-left:0}.p-list--divided .p-list__item{margin-top:0;padding-bottom:.63rem;padding-top:.63rem;border-bottom:1px dotted #cdcdcd}.p-list--divided .p-list__item:last-of-type,.p-list--divided .p-list__item .last-item{border-bottom:0}.is-ticked{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14'%3E%3Ccircle fill='%23666' cx='7' cy='7' r='7'/%3E%3Cpath fill='%23fff' d='M6.1 10.813L2.41 8.105l1.184-1.613L5.9 8.187l4.393-4.394 1.414 1.414z' /%3E%3C/svg%3E");background-position:0 .25rem;background-repeat:no-repeat;padding-left:25px}.p-list--divided .is-ticked{background-position:0 1rem}.p-inline-list{margin-left:0;padding-left:0}.p-inline-list__item{display:inline;list-style:none;margin-right:1.25rem}.p-inline-list__item:last-of-type,.p-inline-list__item .last-item{margin-right:0}.p-inline-list--middot{margin-left:0;padding-left:0}.p-inline-list--middot .p-inline-list__item{display:inline;list-style:none;margin-right:1.25rem;position:relative}.p-inline-list--middot .p-inline-list__item:last-of-type,.p-inline-list--middot .p-inline-list__item .last-item{margin-right:0}.p-inline-list--middot .p-inline-list__item::after{color:#666;content:'\00b7';font-size:1.4rem;line-height:0;position:absolute;right:-1rem;top:.55rem}.p-inline-list--middot .p-inline-list__item:hover::after{color:#666}.p-inline-list--middot .p-inline-list__item:last-of-type::after,.p-inline-list--middot .p-inline-list__item .last-item::after{content:''}.p-list-step{list-style:none;margin-left:60px;padding:0}.p-list-step__title{margin-top:0;position:relative}.p-list-step__title+*{margin-top:0}.p-list-step__item{clear:both;margin-left:0;margin-top:1.5rem;width:100%}.p-list-step__item:first-child{margin-top:.75rem}@media only screen and (min-width:1030px){.p-list-step__item:first-child{margin-top:0}}.p-list-step__bullet{background-color:#666;border-radius:50%;color:#fff;display:inline-block;font-size:1.5rem;height:50px;line-height:50px;margin-bottom:.625rem;margin-left:-60px;margin-right:.34375rem;text-align:center;width:50px}@media only screen and (max-width:1030px){.p-list-step__bullet{position:absolute;top:-5px}}.p-inline-images{display:block;list-style:none;text-align:center}.p-inline-images__item{display:inline-block;margin:2rem;max-width:6rem;text-align:center;vertical-align:middle;width:100%}@media(min-width:768px){.p-inline-images__item{margin:3rem;max-width:11.25rem}}.p-inline-images__item *{width:100%}.p-inline-images__img{display:inline-block;margin:2rem;max-width:6rem;text-align:center;vertical-align:middle;width:100%}@media(min-width:768px){.p-inline-images__img{margin:3rem;max-width:11.25rem}}.p-notification{background-color:#fff;border:0;border-color:#666;border-radius:.125rem;border-style:solid;border-top-width:3px;box-shadow:0 1px 5px 1px rgba(0,0,0,0.2);font-size:1rem;overflow:hidden;padding:.625rem;text-align:center;width:100%}.p-notification__response{background-position:0 4px;background-repeat:no-repeat;background-size:16px 16px;margin:0;text-align:left}.p-notification__status{font-weight:400;margin-right:.3125rem}.p-notification__action{border-bottom:0;margin-left:.3125rem}.p-notification--positive{background-color:#fff;border:0;border-color:#666;border-radius:.125rem;border-style:solid;border-top-width:3px;box-shadow:0 1px 5px 1px rgba(0,0,0,0.2);font-size:1rem;overflow:hidden;padding:.625rem;text-align:center;width:100%;border-color:#0e8420}.p-notification--positive .p-notification__response{background-image:url("data:image/svg+xml,%3Csvg width='17px' height='17px' viewBox='0 0 17 17' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='notification-success' transform='translate(1.000000, 1.000000)'%3E%3Cg id='Page-3---colours'%3E%3Cg id='Notifications---single'%3E%3Cg id='Group'%3E%3Cg id='ICON'%3E%3Ccircle id='circle6710' stroke='%230e8420' stroke-width='1.5' fill='%230e8420' cx='7.2500086' cy='7.2500086' r='7.2500086'%3E%3C/circle%3E%3Cpolygon id='path6712' fill='%23fff' points='11.0502986 4.1734486 10.9843986 4.2311486 6.2496486 8.3783686 3.4740786 5.9974286 2.6350186 6.9463086 6.2503386 10.7500186 11.7500086 4.9627786 11.0502986 4.1734886'%3E%3C/polygon%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E");padding-left:1.5rem}.p-notification--caution{background-color:#fff;border:0;border-color:#666;border-radius:.125rem;border-style:solid;border-top-width:3px;box-shadow:0 1px 5px 1px rgba(0,0,0,0.2);font-size:1rem;overflow:hidden;padding:.625rem;text-align:center;width:100%;border-color:#f99b11}.p-notification--caution .p-notification__response{background-image:url("data:image/svg+xml,%3Csvg width='17px' height='17px' viewBox='0 0 17 17' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='notification-caution' transform='translate(1.000000, 1.000000)'%3E%3Cg id='Page-3---colours'%3E%3Cg id='Notifications---single'%3E%3Cg id='Group'%3E%3Cg id='ICON'%3E%3Ccircle id='circle5432' stroke='%23f99b11' stroke-width='1.5' fill='%23f99b11' cx='7.2500086' cy='7.2500086' r='7.2500086'%3E%3C/circle%3E%3Cpath d='M6.2500086,3.2500086 L6.2500086,8.2500086 L8.2500086,8.2500086 L8.2500086,3.2500086 L6.2500086,3.2500086 L6.2500086,3.2500086 L6.2500086,3.2500086 Z M6.2500086,9.2500086 L6.2500086,11.2500086 L8.2500086,11.2500086 L8.2500086,9.2500086 L6.2500086,9.2500086 L6.2500086,9.2500086 L6.2500086,9.2500086 Z' id='rect5434' fill='%23fff'%3E%3C/path%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E");padding-left:1.5rem}.p-notification--negative{background-color:#fff;border:0;border-color:#666;border-radius:.125rem;border-style:solid;border-top-width:3px;box-shadow:0 1px 5px 1px rgba(0,0,0,0.2);font-size:1rem;overflow:hidden;padding:.625rem;text-align:center;width:100%;border-color:#c7162b}.p-notification--negative .p-notification__response{background-image:url("data:image/svg+xml,%3Csvg width='16px' height='17px' viewBox='0 0 16 17' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg id='Page-3---colours' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='Notifications---single' transform='translate(-215.000000, -271.000000)'%3E%3Cg id='Group' transform='translate(205.000000, 254.000000)'%3E%3Cg id='ICON' transform='translate(10.000000, 17.000000)'%3E%3Crect id='rect6415' x='0' y='0.36218' width='16' height='16'%3E%3C/rect%3E%3Ccircle id='circle6417' stroke='%23c7162b' stroke-width='1.5' fill='%23c7162b' cx='8' cy='8.36218' r='7.2500086'%3E%3C/circle%3E%3Cpath d='M5.00001,5.36218 L11.00001,11.36218' id='path6479-8' stroke='%23fff' stroke-width='1.5'%3E%3C/path%3E%3Cpath d='M11.00001,5.36218 L5.00001,11.36218' id='path6481-8' stroke='%23fff' stroke-width='1.5'%3E%3C/path%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E");padding-left:1.5rem}.p-notification--information{background-color:#fff;border:0;border-color:#666;border-radius:.125rem;border-style:solid;border-top-width:3px;box-shadow:0 1px 5px 1px rgba(0,0,0,0.2);font-size:1rem;overflow:hidden;padding:.625rem;text-align:center;width:100%;border-color:#335280}.p-pull-quote{border:0;margin:2rem 0 1rem;padding-left:2rem;padding-right:1.25rem;position:relative}@media(min-width:768px){.p-pull-quote{margin:1.5rem 0 1.5rem}}.p-pull-quote>p{font-size:1.5rem;font-weight:300;line-height:1.154;color:#111;font-style:normal}@media only screen and (min-width:1030px){.p-pull-quote>p{font-size:1.75rem;line-height:1.286}}.p-pull-quote>p:first-of-type::before{color:#cdcdcd;display:inline-block;font-size:2.134rem;font-weight:bold;line-height:1rem;max-width:1.25rem;content:'\201C\2002';margin-left:-1.5rem;padding-right:1.5rem;position:relative;top:.1rem}@media(min-width:768px){.p-pull-quote>p:first-of-type::before{font-size:2.5rem}}@media(min-width:1030px){.p-pull-quote>p:first-of-type::before{font-size:3rem}}@media(min-width:768px){.p-pull-quote>p:first-of-type::before{margin-left:-1.9rem;padding-right:1.9rem;top:.4rem}}.p-pull-quote>p:last-of-type{margin-bottom:0}.p-pull-quote>p:last-of-type::after{color:#cdcdcd;display:inline-block;font-size:2.134rem;font-weight:bold;line-height:1rem;max-width:1.25rem;content:'\2002\201E';margin-left:.5rem;margin-top:-.5rem;position:absolute}@media(min-width:768px){.p-pull-quote>p:last-of-type::after{font-size:2.5rem}}@media(min-width:1030px){.p-pull-quote>p:last-of-type::after{font-size:3rem}}.p-pull-quote__citation{display:inline-block;font-size:1.25rem;font-style:italic;line-height:1.5;margin-top:.75rem;width:100%}.p-strip{background-color:transparent;clear:both;margin-top:0;padding:2rem 0;width:100%}.p-strip--light{background-color:transparent;clear:both;margin-top:0;padding:2rem 0;width:100%;background-color:#f7f7f7}.p-strip--dark{background-color:transparent;clear:both;margin-top:0;padding:2rem 0;width:100%;background-color:#111;color:#fff}.p-form-validation{color:#111;line-height:1.5;margin-top:1.25rem;position:relative}.p-form-validation .p-form-validation__input{background-position:calc(100% - 1rem) .75rem;background-repeat:no-repeat;padding:.5rem 2.5rem .5rem .75rem}.p-form-validation .p-form-validation__icon{position:relative}.p-form-validation .p-form-validation__icon::after{position:absolute;right:.75rem;top:calc(50% - $sp-x-small)}.p-form-validation__message{font-size:.875rem;margin-top:.5rem}.is-error .p-form-validation__input{background-image:url("https://assets.ubuntu.com/v1/4b0cd7fc-icon-error.svg");border-color:#c7162b}.is-success .p-form-validation__input{background-image:url("https://assets.ubuntu.com/v1/94949185-icon-success.svg");border-color:#0e8420}.is-caution .p-form-validation__input{background-image:url("https://assets.ubuntu.com/v1/db30f04c-icon-caution.svg");border-color:#f99b11}*+*{margin-top:1.25rem}@media screen and (min-width:768px){*+*{margin-top:1.75rem}}@media screen and (min-width:1030px){*+*{margin-top:2rem}}*>p:first-child{margin-top:0}*+h1,*+.p-heading--one{margin-top:2rem}@media screen and (min-width:1030px){*+h1,*+.p-heading--one{margin-top:3.75rem}}*+h2,*+.p-heading--two,*+h3,*+.p-heading--three,*+h4,*+.p-heading--four,*+h5,*+.p-heading--five,*+h6,*+.p-heading--six{margin-top:1.5rem}@media screen and (min-width:1030px){*+h2,*+.p-heading--two,*+h3,*+.p-heading--three,*+h4,*+.p-heading--four,*+h5,*+.p-heading--five,*+h6,*+.p-heading--six{margin-top:2rem}}*+h2+*,*+.p-heading--two+*,*+h3+*,*+.p-heading--three+*,*+h4+*,*+.p-heading--four+*{margin-top:.5rem}@media screen and (min-width:1030px){*+h2+*,*+.p-heading--two+*,*+h3+*,*+.p-heading--three+*,*+h4+*,*+.p-heading--four+*{margin-top:1rem}}.u-float--right{float:right !important}.u-float--left{float:left !important}.u-float-right{float:right !important}@media(max-width:620px){.u-float-right--small{float:right !important}}@media(min-width:768px) and (max-width:1030px){.u-float-right--medium{float:right !important}}@media(min-width:1030px){.u-float-right--large{float:right !important}}.u-float-left{float:left !important}@media(max-width:620px){.u-float-left--small{float:left !important}}@media(min-width:768px) and (max-width:1030px){.u-float-left--medium{float:left !important}}@media(min-width:1030px){.u-float-left--large{float:left !important}}.u-embedded-media{height:0;max-width:100%;overflow:hidden;padding-bottom:56.25%;position:relative}.u-embedded-media__element{height:100%;left:0;position:absolute;top:0;width:100%}@media only screen and (min-width:768px){.u-equal-height{display:flex}}.u-align--center{justify-content:center !important;text-align:center !important}.u-align--left{justify-content:flex-start !important;text-align:left !important}.u-align--right{justify-content:flex-end !important;text-align:right !important}.u-no-margin{margin:0 !important}.u-no-margin--top{margin-top:0 !important}.u-no-margin--right{margin-right:0 !important}.u-no-margin--bottom{margin-bottom:0 !important}.u-no-margin--left{margin-left:0 !important}.u-no-padding{padding:0 !important}.u-no-padding--top{padding-top:0 !important}.u-no-padding--right{padding-right:0 !important}.u-no-padding--bottom{padding-bottom:0 !important}.u-no-padding--left{padding-left:0 !important}.u-hide{display:none !important}@media screen and (max-width:768px){.u-hide--small{display:none !important}}@media(min-width:768px) and (max-width:1030px){.u-hide--medium{display:none !important}}@media screen and (min-width:1030px){.u-hide--large{display:none !important}}.u-show{display:block !important}@media screen and (max-width:768px){.u-show--small{display:block !important}}@media(min-width:768px) and (max-width:1030px){.u-show--medium{display:block !important}}@media screen and (min-width:1030px){.u-show--large{display:block !important}}.u-off-screen{height:1px !important;left:-10000px !important;overflow:hidden !important;position:absolute !important;top:auto !important;width:1px !important}@media(min-width:768px){.u-vertically-center{align-items:center !important;display:flex !important}}.u-hidden{display:none !important}@media screen and (max-width:768px){.u-hidden--small{display:none !important}}@media(min-width:768px) and (max-width:1030px){.u-hidden--medium{display:none !important}}@media screen and (min-width:1030px){.u-hidden--large{display:none !important}}.u-visible{display:block !important}@media screen and (max-width:768px){.u-visible--small{display:block !important}}@media(min-width:768px) and (max-width:1030px){.u-visible--medium{display:block !important}}@media screen and (min-width:1030px){.u-visible--large{display:block !important}}.collapse{cursor:pointer;display:block}.collapse+input{display:none}.collapse+input+div{display:none}.collapse+input:checked+div{display:block}.cheshire{display:none}.required label:after{color:#e32;content:' *';display:inline}
785\ No newline at end of file
786diff --git a/static/sass/application.scss b/static/sass/application.scss
787index fc671d2..7a3bd95 100644
788--- a/static/sass/application.scss
789+++ b/static/sass/application.scss
790@@ -1,7 +1,4 @@
791-// Import the theme
792-@import '../../node_modules/ubuntu-vanilla-theme/scss/theme';
793-// Run the theme
794-@include ubuntu-vanilla-theme;
795+@import 'node_modules/vanilla-framework/scss/build';
796
797 // Customise theme
798 .collapse{
799@@ -21,4 +18,11 @@
800
801 .cheshire {
802 display: none;
803-}
804\ No newline at end of file
805+}
806+
807+.required label:after {
808+ color: #e32;
809+ content: ' *';
810+ display:inline;
811+}
812+
813diff --git a/static/templates/connecting.html b/static/templates/connecting.html
814index d21a0c1..e0f8fb3 100644
815--- a/static/templates/connecting.html
816+++ b/static/templates/connecting.html
817@@ -12,11 +12,14 @@
818 <body>
819 <div class="wrapper">
820 <div id="main-content" class="inner-wrapper">
821+ <br/>
822 <div class="row no-border" id="grid">
823- <h3>Connecting...</h3>
824- <div class="twelve-col box" style="background-color: #eee">
825- <p>Device is now attempting to connect to <b>{{.Ssid}}</b>. <br/>
826+ <div class="p-notification" id="grid">
827+ <p class="p-notification__response">
828+ <span class="p-notification__status">Connecting...</span>
829+ Device is now attempting to connect to <b>{{.Ssid}}</b>. <br/>
830 You may try to connect to the device through the new connection.</p>
831+ </p>
832 </div>
833 </div>
834 </div>
835diff --git a/static/templates/management.html b/static/templates/management.html
836index 6a95224..ed554fb 100644
837--- a/static/templates/management.html
838+++ b/static/templates/management.html
839@@ -12,73 +12,185 @@
840 <body>
841 <div class="wrapper">
842 <div id="main-content" class="inner-wrapper">
843+ <br/>
844 <div class="row no-border" id="login">
845- <ul class="no-bullets">
846- <li><p>Password:</p></li>
847- <li><input type="password" id="passphrasePage"/></li>
848- <li>
849+ <fieldset>
850+ <div class="p-form-validation" id="passphrasePage_form">
851+ <label for="passphrasePage">Password:</label>
852+ <input class="p-form-validation__input" type="password" id="passphrasePage"/>
853 <input type="checkbox" id="showpassphrasePage" onchange="show_passphrase('Page')"/>
854 <label for="showpassphrasePage">show passphrase</label>
855- </li>
856- <li><input type="button" value="Continue" onclick="authenticate()"/></li>
857- </ul>
858+ </div>
859+ <div class="p-notification--negative cheshire" id="passphrasePage_error">
860+ <p class="p-notification__response">
861+ <span class="p-notification__status">Error:</span> <text id="passphrasePage_error_msg"></text>
862+ </p>
863+ </div>
864+ <button class="p-button--positive" onclick="authenticate()"/>Continue</button>
865+ </fieldset>
866 </div>
867
868- <div class="row no-border" id="grid">
869- <h2>Select Wi-Fi to connect to:</h2>
870- <fieldset>
871- <br/>
872- <table>
873+ {{if eq .Page "ssids"}}
874+ <div class="row no-border" id="grid">
875+ <h2>Select Wi-Fi to connect to:</h2>
876+ <ul class="p-list--divided">
877 {{range $i, $ssid := .Ssids}}
878- <tr>
879- <th scope="row">
880- <label class="collapse" for="radio{{$i}}">{{$ssid}}</label>
881- <input id="radio{{$i}}" type="radio" name="c1">
882- <div class="six-col">
883- <fieldset>
884- <ul class="no-bullets">
885- <li>
886- <input type="hidden" id="ssid{{$i}}" value="{{$ssid}}"/>
887- <label for="passphrase">Enter passphrase:</label>
888- <input type="password" id="passphrase{{$i}}"/>
889- </li>
890- <li>
891- <input type="checkbox" id="showpassphrase{{$i}}" onchange="show_passphrase({{$i}})"/>
892- <label for="showpassphrase">show passphrase</label>
893- </li>
894- <li>
895- <div id="alert{{$i}}" class="cheshire box" style="background-color: #eee">
896- <h3>Caution!</h3>
897- <p>Continuing will disconnect you from the device by taking down its Wi-Fi network.
898- This page will be unavailable. After continuing, you may connect to the device
899- through its new Wi-Fi connection. Proceed?.</p>
900- </div>
901- </li>
902- <li>
903- <input type="button" value="Cancel" class="button--primary" onclick="clear_row({{$i}})"/>
904- <input type="button" id="connect{{$i}}" value="Connect" class="button--primary" onclick="do_connect({{$i}})"/>
905- </li>
906- </ul>
907- </fieldset>
908+ <li class="p-list__item">
909+ <label class="collapse" for="radio{{$i}}">{{$ssid}}</label>
910+ <input id="radio{{$i}}" type="radio" name="c1">
911+ <div>
912+ <br/>
913+ <fieldset>
914+ <input type="hidden" id="ssid{{$i}}" value="{{$ssid}}"/>
915+ <label for="passphrase">Enter passphrase:</label>
916+ <input type="password" id="passphrase{{$i}}"/>
917+ <input type="checkbox" id="showpassphrase{{$i}}" onchange="show_passphrase({{$i}})"/>
918+ <label for="showpassphrase">show passphrase</label>
919+ <br/>
920+ <div id="alert{{$i}}" class="p-notification--caution cheshire">
921+ <p class="p-notification__response">
922+ <span class="p-notification__status">Warning:</span>
923+ Continuing will disconnect you from the device by taking down its Wi-Fi network.
924+ This page will be unavailable. After continuing, you may connect to the device
925+ through its new Wi-Fi connection. Proceed?.
926+ </p>
927 </div>
928- </th>
929- </tr>
930+ <button class="p-button--neutral" onclick="clear_row({{$i}})">Cancel</button>
931+ <input type="button" class="p-button--positive" id="connect{{$i}}" value="Connect" onclick="do_connect({{$i}})"/>
932+ </fieldset>
933+ </div>
934+ </li>
935 {{end}}
936- </table>
937
938- <div class="box" style="background-color: #eee">
939- <h3>Warning</h3>
940- <p>This takes down the device AP and disconnects you. Please reconnect in a minute or two</p>
941- <input type="button" value="Refresh Wi-Fi Access Points" class="button--primary" onclick="do_refresh()"/>
942+ </ul>
943+
944+ <div class="p-notification--caution">
945+ <p class="p-notification__response">
946+ <span class="p-notification__status">Warning:</span>
947+ This takes down the device AP and disconnects you. Please reconnect in a minute or two
948+ </p>
949+ <br/>
950+ <button class="p-button--neutral" onclick="do_refresh()">Refresh Wi-Fi Access Points</button>
951 </div>
952+ </div>
953+
954+ {{else if eq .Page "config"}}
955+ <div class="row no-border" id="grid">
956+ <h2>First configuration</h2>
957+ <br/>
958+ <fieldset>
959+ <h4>Wi-Fi Access Point</h4>
960+ <div class="p-form-validation" id="ssid_form">
961+ <label for="ssid">SSID:</label>
962+ <input class="p-form-validation__input" type="text" id="ssid" required="required" value="{{.Config.Wifi.Ssid}}" placeholder="Enter SSID"/>
963+ </div>
964+ <div class="p-notification--negative cheshire" id="ssid_error">
965+ <p class="p-notification__response">
966+ <span class="p-notification__status">Error:</span> <text id="ssid_error_msg"></text>
967+ </p>
968+ </div>
969+ <div class="p-form-validation required" id="passphrase_form">
970+ <label for="passphrase">Passphrase:</label>
971+ <input class="p-form-validation__input" type="password" id="passphrase" required="required" onblur="validate_password(this.id,'Passphrase',8,63)" value="{{.Config.Wifi.Passphrase}}" placeholder="Enter passphrase"/>
972+ <label for="passphrase_confirm">Confirm passphrase:</label>
973+ <input class="p-form-validation__input" type="password" id="passphrase_confirm" required="required" onblur="validate_password('passphrase','Passphrase',8,63)" placeholder="Confirm passphrase"/>
974+ </div>
975+ <div class="p-notification--negative cheshire" id="passphrase_error">
976+ <p class="p-notification__response">
977+ <span class="p-notification__status">Error:</span> <text id="passphrase_error_msg"></text>
978+ </p>
979+ </div>
980+ <div class="p-form-validation" id="interface_form">
981+ <label for="interface">Interface:</label>
982+ <input type="text" id="interface" required="required" value="{{.Config.Wifi.Interface}}" placeholder="Enter network interface"/>
983+ </div>
984+ <div class="p-notification--negative cheshire" id="interface_error">
985+ <p class="p-notification__response">
986+ <span class="p-notification__status">Error:</span> <text id="interface_error_msg"></text>
987+ </p>
988+ </div>
989+ <label for="country_code">Country Code:</label>
990+ <select id="country_code">
991+ [COUNTRY_CODE_OPTIONS]
992+ </select>
993+ <label for="channel">Channel:</label>
994+ <select id="channel">
995+ <option value="1">1</option>
996+ <option value="2">2</option>
997+ <option value="3">3</option>
998+ <option value="4">4</option>
999+ <option value="5">5</option>
1000+ <option value="6">6</option>
1001+ <option value="7">7</option>
1002+ <option value="8">8</option>
1003+ <option value="9">9</option>
1004+ <option value="10">10</option>
1005+ <option value="11">11</option>
1006+ <option value="12">12</option>
1007+ <option value="13">13</option>
1008+ <option value="14">14</option>
1009+ </select>
1010+ <label for="operation_mode">Operation mode:</label>
1011+ <select id="operation_mode">
1012+ <option value="a">IEEE 802.11a (5 GHz)</option>
1013+ <option value="b">IEEE 802.11b (2.4 GHz)</option>
1014+ <option value="g">IEEE 802.11g (2.4 GHz)</option>
1015+ <option value="ad">IEEE 802.11ad (60 GHz)</option>
1016+ </select>
1017+ </fieldset>
1018
1019+ <br/>
1020+
1021+ <fieldset>
1022+ <h4>Web Portal</h4>
1023+ <div class="p-form-validation required" id="portal_pwd_form">
1024+ <label for="portal_pwd">Portal password:</label>
1025+ <input class="p-form-validation__input" type="password" id="portal_pwd" required="required" value="{{.Config.Portal.Password}}" onblur="validate_password(this.id,'Portal password',8)"/>
1026+ <label for="portal_pwd_confirm">Confirm portal password:</label>
1027+ <input class="p-form-validation__input" type="password" id="portal_pwd_confirm" required="required" onblur="validate_password('portal_pwd','Portal password',8)"/>
1028+ </div>
1029+ <div class="p-notification--negative cheshire" id="portal_pwd_error">
1030+ <p class="p-notification__response">
1031+ <span class="p-notification__status">Error:</span> <text id="portal_pwd_error_msg"></text>
1032+ </p>
1033+ </div>
1034 </fieldset>
1035+
1036+ <br/>
1037+
1038+ <div class="p-notification--caution">
1039+ <p class="p-notification__response">
1040+ <span class="p-notification__status">Warning:</span>
1041+ This may take down the device Wi-Fi AP and disconnect you. Please reconnect in a minute or two
1042+ </p>
1043+ <br/>
1044+ <button class="p-button--positive" onclick="saveConfig()">Apply configuration</button>
1045+ </div>
1046+ <div class="p-notification--negative cheshire" id="saveConfig_error">
1047+ <p class="p-notification__response">
1048+ <span class="p-notification__status">Error:</span><text id="saveConfig_error_msg">Could not save configuration. Server error.</text>
1049+ </p>
1050+ </div>
1051 </div>
1052-
1053- <div class="row no-border" id="refreshingAlert">
1054- <h3>Refreshing Wi-Fi access points...</h3>
1055- <div class="twelve-col box" style="background-color: #eee">
1056- <p>You have been disconnected. The device is refreshing Wi-Fi access points. Please rejoin the device access point and reload this page in a minute or two.</p>
1057+ {{end}}
1058+
1059+ <div class="row no-border">
1060+ <div class="p-notification cheshire" id="refreshingAlert">
1061+ <p class="p-notification__response">
1062+ <span class="p-notification__status">Refreshing Wi-Fi access points...</span>
1063+ You have been disconnected. The device is refreshing Wi-Fi access points.
1064+ Please rejoin the device access point and reload this page in a minute or two.
1065+ </p>
1066+ </div>
1067+ </div>
1068+
1069+ <div class="row no-border">
1070+ <div class="p-notification cheshire" id="savingConfigAlert">
1071+ <p class="p-notification__response">
1072+ <span class="p-notification__status">Saving configuration...</span>
1073+ You have been disconnected. The device is saving configuration.
1074+ This takes down the device AP and disconnects you. Please reconnect in a minute or two
1075+ </p>
1076 </div>
1077 </div>
1078
1079@@ -95,7 +207,37 @@
1080 $(document).ready(function() {
1081 $('#grid').css('display', 'none')
1082 $('#refreshingAlert').css('display', 'none')
1083+ $('#savingConfigAlert').css('display', 'none')
1084+
1085+ // enable submit password when enter is pressed for login div
1086+ $('#passphrasePage').keydown(function(event) {
1087+ if (event.keyCode == 13) {
1088+ authenticate()
1089+ return false;
1090+ }
1091+ });
1092+
1093+ {{if eq .Page "config"}}
1094+ // config specific initial values for combo boxes
1095+ $('#channel').val('{{.Config.Wifi.Channel}}')
1096+ $('#country_code').val('{{.Config.Wifi.CountryCode}}')
1097+ $('#operation_mode').val('{{.Config.Wifi.OperationMode}}')
1098+ {{end}}
1099+
1100+ // country codes select must be sorted as originally it is, but by value, not by text
1101+ sortCountryCodes();
1102 })
1103+ function sortCountryCodes() {
1104+ var cc_options = $("#country_code option");
1105+ var selected = $("#country_code").val();
1106+
1107+ cc_options.sort(function(a,b) {
1108+ return a.text.localeCompare(b.text)
1109+ })
1110+
1111+ $("#country_code").empty().append(cc_options);
1112+ $("#country_code").val(selected);
1113+ }
1114 function showSsids() {
1115 $('#login').css('display', 'none');
1116 $('#grid').css('display', 'block');
1117@@ -105,26 +247,113 @@
1118 $('#refreshingAlert').css('display', 'block');
1119 }
1120 function authenticate() {
1121- var pw = $('#passphrasePage').val();
1122- if ( pw.length < 8) {
1123- alert("Password must be at least 8 characters long");
1124- } else {
1125- $.ajax({
1126- type: "POST",
1127- url: "/hashit",
1128- data: {Hash: pw}
1129- }).done(function (hashRet) {
1130- console.log("in ajax done.", hashRet);
1131- hash = JSON.parse(hashRet);
1132- console.log(hash)
1133- if (hash.HashMatch) {
1134- showSsids();
1135- } else {
1136- alert("Your password does not match, please try again");
1137- }
1138- })
1139- }
1140+ $.ajax({
1141+ type: "POST",
1142+ url: "/hashit",
1143+ data: {Hash: $('#passphrasePage').val()}
1144+ }).done(function (hashRet) {
1145+ console.log("in ajax done.", hashRet);
1146+ hash = JSON.parse(hashRet);
1147+ console.log(hash)
1148+ if (hash.HashMatch) {
1149+ showSsids();
1150+ } else {
1151+ $('#passphrasePage_form').addClass('is-error')
1152+ $('#passphrasePage_error_msg').text('Your password does not match, please try again')
1153+ $('#passphrasePage_error').css('display', 'block')
1154+ }
1155+ })
1156 }
1157+ function saveConfig() {
1158+ if ($('#ssid').val().length == 0) {
1159+ $('#ssid_form').addClass('is-error')
1160+ $('#ssid_error_msg').text('SSID cannot be empty')
1161+ $('#ssid_error').css('display', 'block')
1162+ $('#saveConfig_error').css('display', 'block')
1163+ $('#saveConfig_error_msg').text('Please, correct SSID field error before applying configuration')
1164+ return
1165+ } else {
1166+ $('#ssid_form').removeClass('is-error')
1167+ $('#ssid_error').css('display', 'none')
1168+ }
1169+
1170+ if (!validate_password('passphrase', 'Passphrase', 8, 63)) {
1171+ $('#saveConfig_error').css('display', 'block')
1172+ $('#saveConfig_error_msg').text('Please, correct Wi-Fi passphrase error before applying configuration')
1173+ return
1174+ }
1175+
1176+ if ($('#interface').val().length == 0) {
1177+ $('#interface_form').addClass('is-error')
1178+ $('#interface_error_msg').text('Interface cannot be empty')
1179+ $('#interface_error').css('display', 'block')
1180+ $('#saveConfig_error').css('display', 'block')
1181+ $('#saveConfig_error_msg').text('Please, correct Wi-Fi interface error before applying configuration')
1182+ return
1183+ } else {
1184+ $('#interface_form').removeClass('is-error')
1185+ $('#interface_error').css('display', 'none')
1186+ }
1187+
1188+ if (!validate_password('portal_pwd', 'Portal password', 8)) {
1189+ $('#saveConfig_error').css('display', 'block')
1190+ $('#saveConfig_error_msg').text('Please, correct Portal password error before applying configuration')
1191+ return
1192+ }
1193+ $.ajax({
1194+ type: "POST",
1195+ url: "/config",
1196+ data: {
1197+ Ssid: $('#ssid').val(),
1198+ Passphrase: $('#passphrase').val(),
1199+ Interface:$('#interface').val(),
1200+ CountryCode: $('#country_code').val(),
1201+ Channel: $('#channel').val(),
1202+ OperationMode: $('#operation_mode').val(),
1203+ PortalPassword: $('#portal_pwd').val()
1204+ }
1205+ }).done(function (response) {
1206+ $('#grid').css('display', 'none');
1207+ $('#saveConfig_error').css('display', 'none')
1208+ $('#savingConfigAlert').css('display', 'block')
1209+ }).fail(function(xhr, status, error) {
1210+ $('#saveConfig_error').css('display', 'block')
1211+ $('#saveConfig_error_msg').text(xhr.responseText)
1212+ });
1213+ }
1214+ // function to confirm password, and confirmation are equals.
1215+ // tag param is the name of primary input field for password.
1216+ // title human readable name of the field validating here
1217+ // n param is the min length to consider
1218+ // m param is the max length allowed
1219+ function validate_password(tag, title, n, m) {
1220+ var pwd = document.getElementById(tag)
1221+ var pwd_confirm = document.getElementById(tag + '_confirm')
1222+ //validate length
1223+ if (n !== undefined && pwd.value.length < n) {
1224+ $('#' + tag + "_form").addClass('is-error')
1225+ $('#' + tag + '_error_msg').text( title + ' is too short. Must have ' + n + ' characters at least')
1226+ $('#' + tag + '_error').css('display', 'block')
1227+ return false
1228+ }
1229+ if (m !== undefined && pwd.value.length > m) {
1230+ $('#' + tag + "_form").addClass('is-error')
1231+ $('#' + tag + '_error_msg').text( title + ' is too long. Must have ' + m + ' characters at most')
1232+ $('#' + tag + '_error').css('display', 'block')
1233+ return false
1234+ }
1235+ //validate confirm password field matches original
1236+ if (pwd.value != pwd_confirm.value) {
1237+ $('#' + tag + "_form").addClass('is-error')
1238+ $('#' + tag + '_error_msg').text('Passwords don\'t match')
1239+ $('#' + tag + '_error').css('display', 'block')
1240+ return false
1241+ }
1242+ // default
1243+ $('#' + tag + "_form").removeClass('is-error')
1244+ $('#' + tag + '_error').css('display', 'none')
1245+ return true
1246+ }
1247 </script>
1248
1249
1250diff --git a/static/templates/operational.html b/static/templates/operational.html
1251index 1d859c5..27d4f94 100644
1252--- a/static/templates/operational.html
1253+++ b/static/templates/operational.html
1254@@ -11,30 +11,52 @@
1255 <body>
1256 <div class="wrapper">
1257 <div id="main-content" class="inner-wrapper">
1258- <div class="row no-border" id="login">
1259- <ul class="no-bullets">
1260- <li><p>Password:</p></li>
1261- <li><input type="password" id="passphrasePage"/></li>
1262- <li>
1263- <input type="checkbox" id="showpassphrasePage" onchange="show_passphrase('Page')"/>
1264- <label for="showpassphrasePage">show passphrase</label>
1265- </li>
1266- <li><input type="button" value="Continue" onclick="authenticate()"/></li>
1267- </ul>
1268- </div>
1269- <div class="row no-border" id="grid">
1270- <h2>Connected!</h2>
1271- <p>The device is connected to an external WiFi AP</p>
1272- <p>Click below to disconnect. Then, join the device Wifi AP, where you can select a new external AP to connect to.</p>
1273- <input type="button" id="disconnect" value="Disconnect from Wifi" class="button--primary" onclick="disconnect()"/>
1274- </div>
1275- </div>
1276+ <br/>
1277+ <div class="row no-border" id="login">
1278+ <fieldset>
1279+ <div class="p-form-validation" id="passphrasePage_form">
1280+ <label for="passphrasePage">Password:</label>
1281+ <input class="p-form-validation__input" type="password" id="passphrasePage"/>
1282+ <input type="checkbox" id="showpassphrasePage" onchange="show_passphrase('Page')"/>
1283+ <label for="showpassphrasePage">show passphrase</label>
1284+ </div>
1285+ <div class="p-notification--negative cheshire" id="passphrasePage_error">
1286+ <p class="p-notification__response">
1287+ <span class="p-notification__status">Error:</span> <text id="passphrasePage_error_msg"></text>
1288+ </p>
1289+ </div>
1290+ <button class="p-button--positive" onclick="authenticate()"/>Continue</button>
1291+ </fieldset>
1292+ </div>
1293+
1294+ <div class="row no-border">
1295+ <div class="p-notification--positive" id="grid">
1296+ <p class="p-notification__response">
1297+ <span class="p-notification__status">Connected!</span>
1298+ The device is connected to an external WiFi AP.<br/>
1299+ Click below to disconnect. Then, join the device Wifi AP,
1300+ where you can select a new external AP to connect to.
1301+ </p>
1302+ <br/>
1303+ <button class="p-button--negative" id="disconnect" onclick="disconnect()">Disconnect from Wi-Fi</button>
1304+ </div>
1305+ </div>
1306+
1307 </div>
1308
1309 <script>
1310- $(document).ready(function(){
1311- $('#grid').css('display', 'none')
1312+ $(document).ready(function(){
1313+ $('#grid').css('display', 'none')
1314+
1315+ // enable submit password when enter is pressed for login div
1316+ $('#passphrasePage').keydown(function(event) {
1317+ if (event.keyCode == 13) {
1318+ authenticate()
1319+ return false;
1320+ }
1321+ });
1322 })
1323+
1324 function showOper() {
1325 $('#login').css('display', 'none');
1326 $('#grid').css('display', 'block');
1327@@ -45,37 +67,33 @@
1328 document.getElementById('passphrase'+i).type = type
1329 }
1330
1331- function authenticate() {
1332- var pw = $('#passphrasePage').val();
1333- if ( pw.length < 8) {
1334- alert("Password must be at least 8 characters long");
1335- } else {
1336- $.ajax({
1337- type: "POST",
1338- url: "/hashit",
1339- data: {Hash: pw}
1340- }).done(function (hashRet) {
1341- console.log("in ajax done.", hashRet);
1342- hash = JSON.parse(hashRet);
1343- console.log(hash)
1344- if (hash.HashMatch) {
1345- showOper();
1346- } else {
1347- alert("Your password does not match, please try again");
1348- }
1349- })
1350+ function authenticate() {
1351+ $.ajax({
1352+ type: "POST",
1353+ url: "/hashit",
1354+ data: {Hash: $('#passphrasePage').val()}
1355+ }).done(function (hashRet) {
1356+ console.log("in ajax done.", hashRet);
1357+ hash = JSON.parse(hashRet);
1358+ console.log(hash)
1359+ if (hash.HashMatch) {
1360+ showOper();
1361+ } else {
1362+ $('#passphrasePage_form').addClass('is-error')
1363+ $('#passphrasePage_error_msg').text('Your password does not match, please try again')
1364+ $('#passphrasePage_error').css('display', 'block')
1365 }
1366+ })
1367 }
1368
1369-function disconnect() {
1370- $.ajax({
1371- url: "/disconnect"
1372- }).done(function () {
1373- console.log("in ajax done.");
1374-
1375- })
1376-}
1377+ function disconnect() {
1378+ $.ajax({
1379+ url: "/disconnect"
1380+ }).done(function () {
1381+ console.log("in ajax done.");
1382
1383+ })
1384+ }
1385 </script>
1386
1387 </body>
1388diff --git a/utils/config.go b/utils/config.go
1389new file mode 100644
1390index 0000000..6aac0f3
1391--- /dev/null
1392+++ b/utils/config.go
1393@@ -0,0 +1,235 @@
1394+package utils
1395+
1396+import (
1397+ "encoding/json"
1398+ "fmt"
1399+ "io/ioutil"
1400+ "log"
1401+ "os"
1402+ "path/filepath"
1403+ "strconv"
1404+ "strings"
1405+
1406+ "launchpad.net/wifi-connect/wifiap"
1407+)
1408+
1409+var configFile = filepath.Join(os.Getenv("SNAP_COMMON"), "pre-config.json")
1410+var mustConfigFlagFile = filepath.Join(os.Getenv("SNAP_COMMON"), ".config_done.flag")
1411+
1412+var wifiapClient wifiap.Operations = wifiap.DefaultClient()
1413+
1414+// Config this project config got from wifi-ap + custom wifi-connect params
1415+type Config struct {
1416+ Wifi *WifiConfig
1417+ Portal *PortalConfig
1418+}
1419+
1420+// WifiConfig config specific parameters for wifi configuration
1421+type WifiConfig struct {
1422+ Ssid string `json:"wifi.ssid"`
1423+ Passphrase string `json:"wifi.security-passphrase"`
1424+ Interface string `json:"wifi.interface"`
1425+ CountryCode string `json:"wifi.country-code"`
1426+ Channel int `json:"wifi.channel"`
1427+ OperationMode string `json:"wifi.operation-mode"`
1428+}
1429+
1430+// PortalConfig config specific parameters for portals configuration
1431+type PortalConfig struct {
1432+ Password string //`json:"portal.password"`
1433+ NoResetCredentials bool `json:"portal.no-reset-creds"`
1434+ NoOperational bool `json:"portal.no-operational"`
1435+}
1436+
1437+func (c *Config) String() string {
1438+ s := []string{
1439+ strings.Join([]string{"--Wifi--", c.Wifi.String()}, "\n"),
1440+ strings.Join([]string{"--Portal--", c.Portal.String()}, "\n"),
1441+ }
1442+ return fmt.Sprintf(strings.Join(s, "\n"))
1443+}
1444+
1445+func (c *PortalConfig) String() string {
1446+ s := []string{
1447+ strings.Join([]string{"Password: ", c.Password}, " "),
1448+ strings.Join([]string{"NoResetCredentials:", strconv.FormatBool(c.NoResetCredentials)}, " "),
1449+ strings.Join([]string{"NoOperational:", strconv.FormatBool(c.NoOperational)}, " "),
1450+ }
1451+ return fmt.Sprintf(strings.Join(s, "\n"))
1452+}
1453+
1454+func (c *WifiConfig) String() string {
1455+ s := []string{
1456+ strings.Join([]string{"Ssid: ", c.Ssid}, " "),
1457+ strings.Join([]string{"Passphrase:", c.Passphrase}, " "),
1458+ strings.Join([]string{"Interface:", c.Interface}, " "),
1459+ strings.Join([]string{"CountryCode:", c.CountryCode}, " "),
1460+ strings.Join([]string{"Channel:", strconv.Itoa(c.Channel)}, " "),
1461+ strings.Join([]string{"OperationMode:", c.OperationMode}, " "),
1462+ }
1463+ return fmt.Sprintf(strings.Join(s, "\n"))
1464+}
1465+
1466+func defaultPortalConfig() *PortalConfig {
1467+ return &PortalConfig{
1468+ Password: "",
1469+ NoResetCredentials: false,
1470+ NoOperational: false,
1471+ }
1472+}
1473+
1474+// config currently stored in local json file is completely storable in PortalConfig
1475+// If needed to scale, we could rewrite this method to support a more generic type
1476+func readLocalConfig() (*PortalConfig, error) {
1477+ if _, err := os.Stat(configFile); os.IsNotExist(err) {
1478+ log.Printf("Warn: not found local config file at %v\n", configFile)
1479+ // in case there is no local config file, return a null pointer
1480+ return nil, nil
1481+ }
1482+
1483+ fileContents, err := ioutil.ReadFile(configFile)
1484+ if err != nil {
1485+ return nil, fmt.Errorf("Error reading json config file: %v", err)
1486+ }
1487+
1488+ // parameters not available in config file will be se to default value
1489+ portalConfig := defaultPortalConfig()
1490+ err = json.Unmarshal(fileContents, portalConfig)
1491+ if err != nil {
1492+ return nil, fmt.Errorf("Error unmarshalling json config file contents: %v", err)
1493+ }
1494+
1495+ return portalConfig, nil
1496+}
1497+
1498+func writeLocalConfig(p *PortalConfig) error {
1499+ // the only writable local config param is the password, stored as a hash
1500+ _, err := HashIt(p.Password)
1501+ if err != nil {
1502+ return fmt.Errorf("Could not hash portal password to file: %v", err)
1503+ }
1504+
1505+ return nil
1506+}
1507+
1508+func readRemoteParam(m map[string]interface{}, key string, defaultValue interface{}) interface{} {
1509+ val, ok := m[key]
1510+ if !ok {
1511+ val = defaultValue
1512+ log.Printf("Warning: %v key was not found in remote config", key)
1513+ }
1514+
1515+ return val
1516+}
1517+
1518+func readRemoteConfig() (*WifiConfig, error) {
1519+ settings, err := wifiapClient.Show()
1520+ if err != nil {
1521+ return nil, fmt.Errorf("Error reading wifi-ap remote configuration: %v", err)
1522+ }
1523+
1524+ // NOTE: Preprocessing for the case of wifi.channel.
1525+ // In the case of the channel, it is returned as string from rest api, but we have to convert it
1526+ // as it is handled as int internally.
1527+ // In case wifi-ap provides this in future as int, this could be replaced by
1528+ //
1529+ // readRemoteParam(settings, "wifi.channel", 0).(int)
1530+ channel, err := strconv.Atoi(readRemoteParam(settings, "wifi.channel", "0").(string))
1531+ if err != nil {
1532+ return nil, fmt.Errorf("Could not parse wifi.channel parameter: %v", err)
1533+ }
1534+
1535+ return &WifiConfig{
1536+ Ssid: readRemoteParam(settings, "wifi.ssid", "").(string),
1537+ Passphrase: readRemoteParam(settings, "wifi.security-passphrase", "").(string),
1538+ Interface: readRemoteParam(settings, "wifi.interface", "").(string),
1539+ CountryCode: readRemoteParam(settings, "wifi.country-code", "").(string),
1540+ Channel: channel,
1541+ OperationMode: readRemoteParam(settings, "wifi.operation-mode", "").(string),
1542+ }, nil
1543+}
1544+
1545+func writeRemoteConfig(wc *WifiConfig) error {
1546+ params := make(map[string]interface{})
1547+ params["wifi.ssid"] = wc.Ssid
1548+ params["wifi.security-passphrase"] = wc.Passphrase
1549+ params["wifi.interface"] = wc.Interface
1550+ params["wifi.country-code"] = wc.CountryCode
1551+ params["wifi.channel"] = wc.Channel
1552+ params["wifi.operation-mode"] = wc.OperationMode
1553+
1554+ err := wifiapClient.Set(params)
1555+ if err != nil {
1556+ return fmt.Errorf("Error writing remote configuration: %v", err)
1557+ }
1558+
1559+ return nil
1560+}
1561+
1562+// ReadConfig reads all config, remote and local, at the same time
1563+var ReadConfig = func() (*Config, error) {
1564+ wifiConfig, err := readRemoteConfig()
1565+ if err != nil {
1566+ return nil, err
1567+ }
1568+
1569+ portalConfig, err := readLocalConfig()
1570+ if err != nil {
1571+ return nil, err
1572+ }
1573+
1574+ // if local config is nil, fill returning object with default values
1575+ if portalConfig == nil {
1576+ portalConfig = defaultPortalConfig()
1577+ }
1578+
1579+ return &Config{Wifi: wifiConfig, Portal: portalConfig}, nil
1580+}
1581+
1582+// WriteConfig writes all remote and local config at the same time
1583+var WriteConfig = func(c *Config) error {
1584+ previousRemoteConfig, err := readRemoteConfig()
1585+ if err != nil {
1586+ return fmt.Errorf("Error reading current remote config before applying new one: %v", err)
1587+ }
1588+
1589+ // only write remote config if it's different from current
1590+ if *previousRemoteConfig != *c.Wifi {
1591+ err = writeRemoteConfig(c.Wifi)
1592+ if err != nil {
1593+ // if an error happens writing remote config there is no need to restore
1594+ // backup, as nothing shouldn't have been written
1595+ return err
1596+ }
1597+ }
1598+
1599+ err = writeLocalConfig(c.Portal)
1600+ if err != nil {
1601+ // rollback
1602+ if previousRemoteConfig != nil {
1603+ backupErr := writeRemoteConfig(previousRemoteConfig)
1604+ if backupErr != nil {
1605+ return fmt.Errorf("Could not restore previous remote configuration: %v\n after error: %v", backupErr, err)
1606+ }
1607+ }
1608+ return err
1609+ }
1610+
1611+ // write flag file for not asking more times for configuring snap before first use
1612+ if MustSetConfig() {
1613+ err = WriteFlagFile(mustConfigFlagFile)
1614+ if err != nil {
1615+ return fmt.Errorf("Error writing flag file after configuring for a first time")
1616+ }
1617+ }
1618+
1619+ return nil
1620+}
1621+
1622+// MustSetConfig true if one needs to configure snap before continuing
1623+var MustSetConfig = func() bool {
1624+ if _, err := os.Stat(mustConfigFlagFile); os.IsNotExist(err) {
1625+ return true
1626+ }
1627+ return false
1628+}
1629diff --git a/utils/config_test.go b/utils/config_test.go
1630new file mode 100644
1631index 0000000..1802a60
1632--- /dev/null
1633+++ b/utils/config_test.go
1634@@ -0,0 +1,543 @@
1635+package utils
1636+
1637+import (
1638+ "fmt"
1639+ "io/ioutil"
1640+ "os"
1641+ "path/filepath"
1642+ "strconv"
1643+ "sync"
1644+ "testing"
1645+ "time"
1646+
1647+ "gopkg.in/check.v1"
1648+)
1649+
1650+const testLocalConfig = `
1651+{
1652+ "portal.no-reset-creds": true,
1653+ "portal.no-operational": false
1654+}
1655+`
1656+
1657+const testLocalConfigBadEntry = `
1658+{
1659+ "portal.password": "the_password",
1660+ "portal.no-reset-creds": true,
1661+ "bad.parameter": "bad.value",
1662+ "portal.no-operational": false
1663+}
1664+`
1665+
1666+const testLocalEmptyConfig = `
1667+{
1668+}
1669+`
1670+
1671+var testPortalConfig = &PortalConfig{"the_password", true, false}
1672+
1673+var rand uint32
1674+var randmu sync.Mutex
1675+
1676+func Test(t *testing.T) { check.TestingT(t) }
1677+
1678+type S struct{}
1679+
1680+var _ = check.Suite(&S{})
1681+
1682+// ####################
1683+// Testing local config
1684+// ####################
1685+func randomName() string {
1686+ randmu.Lock()
1687+ r := rand
1688+ if r == 0 {
1689+ r = uint32(time.Now().UnixNano() + int64(os.Getpid()))
1690+ }
1691+ r = r*1664525 + 1013904223 // constants from Numerical Recipes
1692+ rand = r
1693+ randmu.Unlock()
1694+ return strconv.Itoa(int(1e9 + r%1e9))[1:]
1695+}
1696+
1697+func createTempFile(content string) (*os.File, error) {
1698+ contentAsBytes := []byte(content)
1699+
1700+ tmpfile, err := ioutil.TempFile("", "config")
1701+ if err != nil {
1702+ return nil, fmt.Errorf("Could not create temp file: %v", err)
1703+ }
1704+
1705+ if _, err := tmpfile.Write(contentAsBytes); err != nil {
1706+ return nil, fmt.Errorf("Could not write contents to temp file: %v", err)
1707+ }
1708+
1709+ if err := tmpfile.Close(); err != nil {
1710+ return nil, fmt.Errorf("Could not close tempfile properly: %v", err)
1711+ }
1712+
1713+ return tmpfile, nil
1714+}
1715+
1716+func verifyLocalConfig(c *check.C, cfg *PortalConfig, expectedPwd string, expectedNoResetCredentials bool, expectedNoOperational bool) {
1717+ c.Assert(cfg.Password, check.Equals, expectedPwd)
1718+ c.Assert(cfg.NoResetCredentials, check.Equals, expectedNoResetCredentials)
1719+ c.Assert(cfg.NoOperational, check.Equals, expectedNoOperational)
1720+}
1721+
1722+func verifyDefaultLocalConfig(c *check.C, cfg *PortalConfig) {
1723+ verifyLocalConfig(c, cfg, "", false, false)
1724+}
1725+
1726+func (s *S) TestReadLocalConfig(c *check.C) {
1727+ f, err := createTempFile(testLocalConfig)
1728+ c.Assert(err, check.IsNil)
1729+
1730+ defer os.Remove(f.Name())
1731+ configFile = f.Name()
1732+
1733+ cfg, err := readLocalConfig()
1734+ c.Assert(err, check.IsNil)
1735+
1736+ verifyLocalConfig(c, cfg, "", true, false)
1737+}
1738+
1739+func (s *S) TestReadLocalConfigBadEntry(c *check.C) {
1740+ // No matter if there are additional not recognized params, only known should be marshalled
1741+ f, err := createTempFile(testLocalConfigBadEntry)
1742+ c.Assert(err, check.IsNil)
1743+
1744+ defer os.Remove(f.Name())
1745+ configFile = f.Name()
1746+
1747+ cfg, err := readLocalConfig()
1748+ c.Assert(err, check.IsNil)
1749+
1750+ verifyLocalConfig(c, cfg, "", true, false)
1751+}
1752+
1753+func (s *S) TestReadLocalEmptyConfig(c *check.C) {
1754+ // No matter if there are additional not recognized params, only known should be marshalled
1755+ f, err := createTempFile(testLocalEmptyConfig)
1756+ c.Assert(err, check.IsNil)
1757+
1758+ defer os.Remove(f.Name())
1759+ configFile = f.Name()
1760+
1761+ cfg, err := readLocalConfig()
1762+ c.Assert(err, check.IsNil)
1763+
1764+ verifyDefaultLocalConfig(c, cfg)
1765+}
1766+
1767+func (s *S) TestReadLocalNotExistingConfig(c *check.C) {
1768+ configFile = "does/not/exists/config.json"
1769+
1770+ cfg, err := readLocalConfig()
1771+ c.Assert(err, check.IsNil)
1772+ c.Assert(cfg, check.IsNil)
1773+}
1774+
1775+func (s *S) TestWriteLocalConfigFileDoesNotExists(c *check.C) {
1776+ mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName())
1777+ defer os.Remove(mustConfigFlagFile)
1778+ HashFile = filepath.Join(os.TempDir(), "portalpwd"+randomName())
1779+ defer os.Remove(HashFile)
1780+
1781+ err := writeLocalConfig(testPortalConfig)
1782+ c.Assert(err, check.IsNil)
1783+
1784+ ok, err := MatchingHash(testPortalConfig.Password)
1785+ c.Assert(err, check.IsNil)
1786+ c.Assert(ok, check.Equals, true)
1787+}
1788+
1789+func (s *S) TestWriteLocalConfigFiletExists(c *check.C) {
1790+ mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName())
1791+ defer os.Remove(mustConfigFlagFile)
1792+
1793+ f, err := createTempFile("whateverbadpasswordhash")
1794+ c.Assert(err, check.IsNil)
1795+
1796+ defer os.Remove(f.Name())
1797+ HashFile = f.Name()
1798+
1799+ err = writeLocalConfig(testPortalConfig)
1800+ c.Assert(err, check.IsNil)
1801+
1802+ ok, err := MatchingHash(testPortalConfig.Password)
1803+ c.Assert(err, check.IsNil)
1804+ c.Assert(ok, check.Equals, true)
1805+}
1806+
1807+// #####################
1808+// Testing remote config
1809+// #####################
1810+type wifiapClientMock struct {
1811+ m map[string]interface{}
1812+}
1813+
1814+func (c *wifiapClientMock) Show() (map[string]interface{}, error) {
1815+ return c.m, nil
1816+}
1817+
1818+func (c *wifiapClientMock) Enable() error {
1819+ return nil
1820+}
1821+
1822+func (c *wifiapClientMock) Disable() error {
1823+ return nil
1824+}
1825+
1826+func (c *wifiapClientMock) Enabled() (bool, error) {
1827+ return true, nil
1828+}
1829+
1830+func (c *wifiapClientMock) SetSsid(string) error {
1831+ return nil
1832+}
1833+
1834+func (c *wifiapClientMock) SetPassphrase(string) error {
1835+ return nil
1836+}
1837+
1838+func (c *wifiapClientMock) Set(map[string]interface{}) error {
1839+ return nil
1840+}
1841+
1842+func (s *S) TestReadRemoteConfig(c *check.C) {
1843+ wifiapClient = &wifiapClientMock{
1844+ m: map[string]interface{}{
1845+ "dhcp.lease-time": "12h",
1846+ "dhcp.range-start": "10.0.60.2",
1847+ "dhcp.range-stop": "10.0.60.199",
1848+ "disabled": true,
1849+ "share.disabled": false,
1850+ "share.network-interface": "wlp2s0",
1851+ "wifi.address": "10.0.60.1",
1852+ "wifi.channel": "6", // in real environment, channel is returned as string
1853+ "wifi.hostapd-driver": "nl80211",
1854+ "wifi.interface": "wlp2s0",
1855+ "wifi.interface-mode": "direct",
1856+ "wifi.country-code": "0x31",
1857+ "wifi.netmask": "255.255.255.0",
1858+ "wifi.operation-mode": "g",
1859+ "wifi.security": "wpa2",
1860+ "wifi.security-passphrase": "17Soj8/Sxh14lcpD",
1861+ "wifi.ssid": "Ubuntu",
1862+ },
1863+ }
1864+
1865+ cfg, err := readRemoteConfig()
1866+ c.Assert(err, check.IsNil)
1867+
1868+ expectedCfg := &WifiConfig{
1869+ Ssid: "Ubuntu",
1870+ Passphrase: "17Soj8/Sxh14lcpD",
1871+ Interface: "wlp2s0",
1872+ CountryCode: "0x31",
1873+ Channel: 6,
1874+ OperationMode: "g",
1875+ }
1876+
1877+ c.Assert(*cfg, check.Equals, *expectedCfg)
1878+}
1879+
1880+func (s *S) TestReadRemoteConfigNotAllParams(c *check.C) {
1881+ wifiapClient = &wifiapClientMock{
1882+ m: map[string]interface{}{
1883+ "dhcp.lease-time": "12h",
1884+ "dhcp.range-start": "10.0.60.2",
1885+ "dhcp.range-stop": "10.0.60.199",
1886+ "share.disabled": false,
1887+ "share.network-interface": "wlp2s0",
1888+ "wifi.address": "10.0.60.1",
1889+ "wifi.hostapd-driver": "nl80211",
1890+ "wifi.interface": "wlp2s0",
1891+ "wifi.interface-mode": "direct",
1892+ "wifi.country-code": "0x31",
1893+ "wifi.netmask": "255.255.255.0",
1894+ "wifi.security": "wpa2",
1895+ "wifi.ssid": "Ubuntu",
1896+ },
1897+ }
1898+
1899+ cfg, err := readRemoteConfig()
1900+ c.Assert(err, check.IsNil)
1901+
1902+ expectedCfg := &WifiConfig{
1903+ Ssid: "Ubuntu",
1904+ Passphrase: "",
1905+ Interface: "wlp2s0",
1906+ CountryCode: "0x31",
1907+ Channel: 0,
1908+ OperationMode: "",
1909+ }
1910+
1911+ c.Assert(*cfg, check.Equals, *expectedCfg)
1912+}
1913+
1914+func (s *S) TestReadEmptyRemoteConfig(c *check.C) {
1915+ wifiapClient = &wifiapClientMock{}
1916+
1917+ cfg, err := readRemoteConfig()
1918+ c.Assert(err, check.IsNil)
1919+
1920+ expectedCfg := &WifiConfig{
1921+ Ssid: "",
1922+ Passphrase: "",
1923+ Interface: "",
1924+ CountryCode: "",
1925+ Channel: 0,
1926+ OperationMode: "",
1927+ }
1928+
1929+ c.Assert(*cfg, check.Equals, *expectedCfg)
1930+}
1931+
1932+func (s *S) TestWriteRemoteConfig(c *check.C) {
1933+ wifiapClient = &wifiapClientMock{}
1934+
1935+ err := writeRemoteConfig(&WifiConfig{
1936+ Ssid: "Ubuntu",
1937+ Passphrase: "17Soj8/Sxh14lcpD",
1938+ Interface: "wlp2s0",
1939+ CountryCode: "0x31",
1940+ Channel: 6,
1941+ OperationMode: "g",
1942+ })
1943+
1944+ c.Assert(err, check.IsNil)
1945+}
1946+
1947+// ####################
1948+// Testing whole config
1949+// ####################
1950+func (s *S) TestWriteConfig(c *check.C) {
1951+ wifiapClient = &wifiapClientMock{}
1952+
1953+ HashFile = filepath.Join(os.TempDir(), "portalpwd"+randomName())
1954+ defer os.Remove(HashFile)
1955+
1956+ mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName())
1957+ defer os.Remove(mustConfigFlagFile)
1958+
1959+ c.Assert(MustSetConfig(), check.Equals, true)
1960+
1961+ err := WriteConfig(&Config{
1962+ Wifi: &WifiConfig{
1963+ Ssid: "Ubuntu",
1964+ Passphrase: "17Soj8/Sxh14lcpD",
1965+ Interface: "wlp2s0",
1966+ CountryCode: "0x31",
1967+ Channel: 6,
1968+ OperationMode: "g",
1969+ },
1970+ Portal: &PortalConfig{
1971+ Password: "thepassword",
1972+ NoResetCredentials: true,
1973+ NoOperational: false,
1974+ },
1975+ })
1976+
1977+ c.Assert(err, check.IsNil)
1978+
1979+ ok, err := MatchingHash("thepassword")
1980+ c.Assert(err, check.IsNil)
1981+ c.Assert(ok, check.Equals, true)
1982+
1983+ c.Assert(MustSetConfig(), check.Equals, false)
1984+}
1985+
1986+func (s *S) TestReadConfig(c *check.C) {
1987+ wifiapClient = &wifiapClientMock{
1988+ m: map[string]interface{}{
1989+ "dhcp.lease-time": "12h",
1990+ "dhcp.range-start": "10.0.60.2",
1991+ "dhcp.range-stop": "10.0.60.199",
1992+ "disabled": true,
1993+ "share.disabled": false,
1994+ "share.network-interface": "wlp2s0",
1995+ "wifi.address": "10.0.60.1",
1996+ "wifi.channel": "6", // in real environment, channel is returned as string
1997+ "wifi.hostapd-driver": "nl80211",
1998+ "wifi.interface": "wlp2s0",
1999+ "wifi.interface-mode": "direct",
2000+ "wifi.country-code": "0x31",
2001+ "wifi.netmask": "255.255.255.0",
2002+ "wifi.operation-mode": "g",
2003+ "wifi.security": "wpa2",
2004+ "wifi.security-passphrase": "17Soj8/Sxh14lcpD",
2005+ "wifi.ssid": "Ubuntu",
2006+ },
2007+ }
2008+
2009+ f, err := createTempFile(testLocalConfig)
2010+ c.Assert(err, check.IsNil)
2011+
2012+ defer os.Remove(f.Name())
2013+ configFile = f.Name()
2014+
2015+ expectedWifiConfig := &WifiConfig{
2016+ Ssid: "Ubuntu",
2017+ Passphrase: "17Soj8/Sxh14lcpD",
2018+ Interface: "wlp2s0",
2019+ CountryCode: "0x31",
2020+ Channel: 6,
2021+ OperationMode: "g",
2022+ }
2023+
2024+ expectedPortalConfig := &PortalConfig{
2025+ Password: "",
2026+ NoResetCredentials: true,
2027+ NoOperational: false,
2028+ }
2029+
2030+ cfg, err := ReadConfig()
2031+ c.Assert(err, check.IsNil)
2032+ c.Assert(*cfg.Wifi, check.Equals, *expectedWifiConfig)
2033+ c.Assert(*cfg.Portal, check.Equals, *expectedPortalConfig)
2034+}
2035+
2036+func (s *S) TestMustSetConfig(c *check.C) {
2037+ mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName())
2038+ defer os.Remove(mustConfigFlagFile)
2039+
2040+ HashFile = filepath.Join(os.TempDir(), "portalpwd"+randomName())
2041+ defer os.Remove(HashFile)
2042+
2043+ wifiapClient = &wifiapClientMock{}
2044+
2045+ // config to save
2046+ cfg := &Config{
2047+ Wifi: &WifiConfig{
2048+ Ssid: "Ubuntu",
2049+ Passphrase: "17Soj8/Sxh14lcpD",
2050+ Interface: "wlp2s0",
2051+ CountryCode: "0x31",
2052+ Channel: 6,
2053+ OperationMode: "g",
2054+ },
2055+ Portal: &PortalConfig{
2056+ Password: "the_password",
2057+ NoResetCredentials: true,
2058+ NoOperational: false,
2059+ },
2060+ }
2061+
2062+ c.Assert(MustSetConfig(), check.Equals, true)
2063+
2064+ err := WriteConfig(cfg)
2065+ c.Assert(err, check.IsNil)
2066+
2067+ c.Assert(MustSetConfig(), check.Equals, false)
2068+
2069+ err = WriteConfig(cfg)
2070+ c.Assert(err, check.IsNil)
2071+
2072+ c.Assert(MustSetConfig(), check.Equals, false)
2073+}
2074+
2075+func (s *S) TestConfigDump(c *check.C) {
2076+ cfg := &Config{
2077+ Wifi: &WifiConfig{
2078+ Ssid: "Ubuntu",
2079+ Passphrase: "17Soj8/Sxh14lcpD",
2080+ Interface: "wlp2s0",
2081+ CountryCode: "0x31",
2082+ Channel: 6,
2083+ OperationMode: "g",
2084+ },
2085+ Portal: &PortalConfig{
2086+ Password: "the_password",
2087+ NoResetCredentials: true,
2088+ NoOperational: false,
2089+ },
2090+ }
2091+
2092+ expected := `--Wifi--
2093+Ssid: Ubuntu
2094+Passphrase: 17Soj8/Sxh14lcpD
2095+Interface: wlp2s0
2096+CountryCode: 0x31
2097+Channel: 6
2098+OperationMode: g
2099+--Portal--
2100+Password: the_password
2101+NoResetCredentials: true
2102+NoOperational: false`
2103+
2104+ c.Assert(cfg.String(), check.Equals, expected)
2105+}
2106+
2107+func (s *S) TestRollbackConfigIfFailsWriting(c *check.C) {
2108+ wifiapClient = &wifiapClientMock{
2109+ m: map[string]interface{}{
2110+ "dhcp.lease-time": "12h",
2111+ "dhcp.range-start": "10.0.60.2",
2112+ "dhcp.range-stop": "10.0.60.199",
2113+ "disabled": true,
2114+ "share.disabled": false,
2115+ "share.network-interface": "wlp2s0",
2116+ "wifi.address": "10.0.60.1",
2117+ "wifi.channel": "8", // in real environment, channel is returned as string
2118+ "wifi.hostapd-driver": "nl80211",
2119+ "wifi.interface": "wlp2s0",
2120+ "wifi.interface-mode": "direct",
2121+ "wifi.country-code": "ES",
2122+ "wifi.netmask": "255.255.255.0",
2123+ "wifi.operation-mode": "ad",
2124+ "wifi.security": "wlan1",
2125+ "wifi.security-passphrase": "abc17Soj8/Sxh14lcpD",
2126+ "wifi.ssid": "mySSID",
2127+ },
2128+ }
2129+
2130+ HashFile = filepath.Join(os.TempDir(), "portalpwd"+randomName())
2131+ defer os.Remove(HashFile)
2132+
2133+ mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName())
2134+ defer os.Remove(mustConfigFlagFile)
2135+
2136+ err := WriteFlagFile(mustConfigFlagFile)
2137+ c.Assert(err, check.IsNil)
2138+ c.Assert(MustSetConfig(), check.Equals, false)
2139+
2140+ // read previous remote config file
2141+ remotePreviousConfig, err := readRemoteConfig()
2142+ c.Assert(err, check.IsNil)
2143+
2144+ // config to save
2145+ cfg := &Config{
2146+ Wifi: &WifiConfig{
2147+ Ssid: "Ubuntu",
2148+ Passphrase: "17Soj8/Sxh14lcpD",
2149+ Interface: "wlp2s0",
2150+ CountryCode: "XX",
2151+ Channel: 6,
2152+ OperationMode: "g",
2153+ },
2154+ Portal: &PortalConfig{
2155+ Password: "the_password",
2156+ NoResetCredentials: true,
2157+ NoOperational: false,
2158+ },
2159+ }
2160+
2161+ originalHashIt := HashIt
2162+ HashIt = func(s string) ([]byte, error) { return nil, fmt.Errorf("Error hashing") }
2163+
2164+ // assert write fails
2165+ err = WriteConfig(cfg)
2166+ c.Assert(err, check.NotNil)
2167+
2168+ HashIt = originalHashIt
2169+
2170+ // read remote config after write file failing and verify it's the original one
2171+ remoteCfg, err := readRemoteConfig()
2172+ c.Assert(err, check.IsNil)
2173+ c.Assert(*remoteCfg, check.Equals, *remotePreviousConfig)
2174+
2175+ // verify must set config flag is not changed in the process
2176+ c.Assert(MustSetConfig(), check.Equals, false)
2177+}
2178diff --git a/utils/utils.go b/utils/utils.go
2179index 94850c0..9a77ce3 100644
2180--- a/utils/utils.go
2181+++ b/utils/utils.go
2182@@ -40,7 +40,7 @@ var HashFile = filepath.Join(os.Getenv("SNAP_COMMON"), "hash")
2183
2184 // HashIt writes the hash of the passed password to the HashFile and
2185 // returns the hash and error
2186-func HashIt(s string) ([]byte, error) {
2187+var HashIt = func(s string) ([]byte, error) {
2188 var b []byte
2189 var err error
2190 b, err = bcrypt.GenerateFromPassword([]byte(s), 8)
2191@@ -154,3 +154,12 @@ func RunningOn(address string) bool {
2192 }
2193 return true
2194 }
2195+
2196+// ParseFormParamSingleValue extracts single value from a http post request
2197+func ParseFormParamSingleValue(form map[string][]string, key string) string {
2198+ vals := form[key]
2199+ if len(vals) > 0 {
2200+ return vals[0]
2201+ }
2202+ return ""
2203+}
2204diff --git a/wifiap/wifiap.go b/wifiap/wifiap.go
2205index faf0c15..d81f883 100644
2206--- a/wifiap/wifiap.go
2207+++ b/wifiap/wifiap.go
2208@@ -45,7 +45,7 @@ func DefaultClient() *Client {
2209 return &Client{restClient: defaultRestClient()}
2210 }
2211
2212-// Operations interface enables mock testing
2213+// Operations interface defining operations implemented by wifiap client
2214 type Operations interface {
2215 Show() (map[string]interface{}, error)
2216 Enabled() (bool, error)
2217@@ -53,6 +53,7 @@ type Operations interface {
2218 Disable() error
2219 SetSsid(string) error
2220 SetPassphrase(string) error
2221+ Set(map[string]interface{}) error
2222 }
2223
2224 func defaultServiceURI() string {
2225@@ -205,3 +206,22 @@ func (client *Client) SetPassphrase(passphrase string) error {
2226
2227 return nil
2228 }
2229+
2230+// Set sets a group of parameters in wifi-ap
2231+func (client *Client) Set(params map[string]interface{}) error {
2232+ b, err := json.Marshal(params)
2233+ if err != nil {
2234+ return fmt.Errorf("Error when marshalling input parameters: %q", err)
2235+ }
2236+
2237+ response, err := client.restClient.sendHTTPRequest(defaultServiceURI(), "POST", bytes.NewReader(b))
2238+ if err != nil {
2239+ return fmt.Errorf("wifi-ap set operation failed: %q", err)
2240+ }
2241+
2242+ if response.StatusCode != http.StatusOK || response.Status != http.StatusText(http.StatusOK) {
2243+ return fmt.Errorf("Failed to set configuration, service returned: %d (%s)", response.StatusCode, response.Status)
2244+ }
2245+
2246+ return nil
2247+}
2248diff --git a/wifiap/wifiap_test.go b/wifiap/wifiap_test.go
2249index 6fa4207..e1f4279 100644
2250--- a/wifiap/wifiap_test.go
2251+++ b/wifiap/wifiap_test.go
2252@@ -21,6 +21,7 @@ import (
2253 "fmt"
2254 "io/ioutil"
2255 "net/http"
2256+ "reflect"
2257 "strings"
2258 "testing"
2259 )
2260@@ -148,7 +149,7 @@ func TestShow(t *testing.T) {
2261 }
2262
2263 // Testing Enable()
2264-func validateHeaders(m map[string]string, req *http.Request) error {
2265+func validateHeaders(m map[string]interface{}, req *http.Request) error {
2266 buf, _ := ioutil.ReadAll(req.Body)
2267 var headers map[string]interface{}
2268 if err := json.Unmarshal(buf, &headers); err != nil {
2269@@ -160,9 +161,20 @@ func validateHeaders(m map[string]string, req *http.Request) error {
2270 return fmt.Errorf("Expected %v headers", n)
2271 }
2272
2273- for key, value := range m {
2274- if headers[key] != value {
2275- return fmt.Errorf("Header '%v' has not valid value", key)
2276+ for key, mValue := range m {
2277+ hValue := headers[key]
2278+ if hValue != mValue {
2279+ // evaluate the special case of hValue unmarshalled as float64
2280+ if reflect.TypeOf(hValue) != reflect.TypeOf(mValue) {
2281+ switch hValue := hValue.(type) {
2282+ case float64:
2283+ if int(hValue) != mValue.(int) {
2284+ return fmt.Errorf("Header '%v' has not valid value. Expected %v but has %v", key, mValue, hValue)
2285+ }
2286+ }
2287+ } else {
2288+ return fmt.Errorf("Header '%v' has not valid value. Expected %v but has %v", key, mValue, hValue)
2289+ }
2290 }
2291 }
2292
2293@@ -181,7 +193,7 @@ func (mock *mockTransportEnable) Do(req *http.Request) (*http.Response, error) {
2294 return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method)
2295 }
2296
2297- err := validateHeaders(map[string]string{"disabled": "false"}, req)
2298+ err := validateHeaders(map[string]interface{}{"disabled": "false"}, req)
2299 if err != nil {
2300 return nil, err
2301 }
2302@@ -238,7 +250,7 @@ func (mock *mockTransportDisable) Do(req *http.Request) (*http.Response, error)
2303 return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method)
2304 }
2305
2306- err := validateHeaders(map[string]string{"disabled": "true"}, req)
2307+ err := validateHeaders(map[string]interface{}{"disabled": "true"}, req)
2308 if err != nil {
2309 return nil, err
2310 }
2311@@ -363,7 +375,7 @@ func (mock *mockTransportSetSsid) Do(req *http.Request) (*http.Response, error)
2312 return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method)
2313 }
2314
2315- err := validateHeaders(map[string]string{"wifi.ssid": "MySsid"}, req)
2316+ err := validateHeaders(map[string]interface{}{"wifi.ssid": "MySsid"}, req)
2317 if err != nil {
2318 return nil, err
2319 }
2320@@ -401,7 +413,7 @@ func (mock *mockTransportSetPassphrase) Do(req *http.Request) (*http.Response, e
2321 return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method)
2322 }
2323
2324- err := validateHeaders(map[string]string{"wifi.security": "wpa2", "wifi.security-passphrase": "passphrase123"}, req)
2325+ err := validateHeaders(map[string]interface{}{"wifi.security": "wpa2", "wifi.security-passphrase": "passphrase123"}, req)
2326 if err != nil {
2327 return nil, err
2328 }
2329@@ -424,3 +436,79 @@ func TestSetPassphrase(t *testing.T) {
2330 t.Errorf("Failed to set passphrase: %v\n", err)
2331 }
2332 }
2333+
2334+type mockTransportSet struct{}
2335+
2336+func (mock *mockTransportSet) Do(req *http.Request) (*http.Response, error) {
2337+
2338+ url := req.URL.String()
2339+ if url != "http://unix/v1/configuration" {
2340+ return nil, fmt.Errorf("Not valid request URL: %v", url)
2341+ }
2342+
2343+ if req.Method != "POST" {
2344+ return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method)
2345+ }
2346+
2347+ err := validateHeaders(map[string]interface{}{
2348+ "dhcp.lease-time": "12h",
2349+ "dhcp.range-start": "10.0.60.2",
2350+ "dhcp.range-stop": "10.0.60.199",
2351+ "disabled": true,
2352+ "share.disabled": false,
2353+ "share.network-interface": "wlp2s0",
2354+ "wifi.address": "10.0.60.1",
2355+ "wifi.channel": 6,
2356+ "wifi.hostapd-driver": "nl80211",
2357+ "wifi.interface": "wlp2s0",
2358+ "wifi.interface-mode": "direct",
2359+ "wifi.country-code": "0x31",
2360+ "wifi.netmask": "255.255.255.0",
2361+ "wifi.operation-mode": "g",
2362+ "wifi.security": "wpa2",
2363+ "wifi.security-passphrase": "17Soj8/Sxh14lcpD",
2364+ "wifi.ssid": "Ubuntu",
2365+ }, req)
2366+ if err != nil {
2367+ return nil, err
2368+ }
2369+
2370+ rawBody := `{"result":{},"status":"OK","status-code":200,"type":"sync"}`
2371+
2372+ response := http.Response{
2373+ StatusCode: 200,
2374+ Status: "200 OK",
2375+ Body: ioutil.NopCloser(strings.NewReader(rawBody)),
2376+ }
2377+
2378+ return &response, nil
2379+}
2380+
2381+func TestSet(t *testing.T) {
2382+
2383+ params := map[string]interface{}{
2384+ "dhcp.lease-time": "12h",
2385+ "dhcp.range-start": "10.0.60.2",
2386+ "dhcp.range-stop": "10.0.60.199",
2387+ "disabled": true,
2388+ "share.disabled": false,
2389+ "share.network-interface": "wlp2s0",
2390+ "wifi.address": "10.0.60.1",
2391+ "wifi.channel": 6,
2392+ "wifi.hostapd-driver": "nl80211",
2393+ "wifi.interface": "wlp2s0",
2394+ "wifi.interface-mode": "direct",
2395+ "wifi.country-code": "0x31",
2396+ "wifi.netmask": "255.255.255.0",
2397+ "wifi.operation-mode": "g",
2398+ "wifi.security": "wpa2",
2399+ "wifi.security-passphrase": "17Soj8/Sxh14lcpD",
2400+ "wifi.ssid": "Ubuntu",
2401+ }
2402+
2403+ client := NewClient(&mockTransportSet{})
2404+ err := client.Set(params)
2405+ if err != nil {
2406+ t.Errorf("Failed to set params: %v\n", err)
2407+ }
2408+}

Subscribers

People subscribed via source and target branches