diff --git a/.github/workflows/arm.yml b/.github/workflows/arm.yml
index f7cc44c..7f8d11a 100644
--- a/.github/workflows/arm.yml
+++ b/.github/workflows/arm.yml
@@ -28,7 +28,7 @@ jobs:
export DEBIAN_FRONTEND=noninteractive
ln -fs /usr/share/zoneinfo/America/New_York /etc/localtime
apt update
- apt install -y curl lsb-release sudo clang
+ apt install -y curl lsb-release sudo clang git
RELEASE_DOT=$(lsb_release -sr)
RELEASE_NUM=${RELEASE_DOT//[-._]/}
RELEASE_NAME=$(lsb_release -sc)
diff --git a/.gitignore b/.gitignore
index f60c665..c4dfa02 100644
--- a/.gitignore
+++ b/.gitignore
@@ -167,3 +167,6 @@ Pods
Brewfile.lock.json
Package.resolved
Documentation/Reference/README.md
+
+*.dump
+*.dump.zip
\ No newline at end of file
diff --git a/.swiftformat b/.swiftformat
index 02d7018..698077f 100644
--- a/.swiftformat
+++ b/.swiftformat
@@ -1,4 +1,5 @@
--indent 2
--header strip
--commas inline
+--disable wrapMultilineStatementBraces
--exclude .build, DerivedData
diff --git a/Configuration/etc/nginx/sites-available/orchardnest.conf b/Configuration/etc/nginx/sites-available/orchardnest.conf
new file mode 100644
index 0000000..0dfb6a9
--- /dev/null
+++ b/Configuration/etc/nginx/sites-available/orchardnest.conf
@@ -0,0 +1,21 @@
+server {
+ server_name orchardnest.com;
+ listen 80;
+
+ root /home/orchardnest/app/Public;
+
+ location / {
+ try_files $uri @proxy;
+ }
+
+ location @proxy {
+ proxy_pass http://127.0.0.1:8080;
+ proxy_pass_header Server;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_pass_header Server;
+ proxy_connect_timeout 3s;
+ proxy_read_timeout 10s;
+ }
+}
diff --git a/Configuration/etc/supervisor/conf.d/orchardnestd.conf b/Configuration/etc/supervisor/conf.d/orchardnestd.conf
new file mode 100644
index 0000000..16dbb09
--- /dev/null
+++ b/Configuration/etc/supervisor/conf.d/orchardnestd.conf
@@ -0,0 +1,7 @@
+[program:orchardnestd]
+command=/home/orchardnest/app/.build/release/orchardnestd serve --env production
+directory=/home/orchardnest/app
+user=orchardnest
+environment=DATABASE_URL='postgres://orchardnest:12345@localhost/orchardnest'
+stdout_logfile=/var/log/supervisor/%(program_name)-stdout.log
+stderr_logfile=/var/log/supervisor/%(program_name)-stderr.log
diff --git a/Configuration/etc/supervisor/conf.d/orchardnestq.conf b/Configuration/etc/supervisor/conf.d/orchardnestq.conf
new file mode 100644
index 0000000..98db60c
--- /dev/null
+++ b/Configuration/etc/supervisor/conf.d/orchardnestq.conf
@@ -0,0 +1,7 @@
+[program:orchardnestq]
+command=/home/orchardnest/app/.build/release/orchardnestd queues --scheduled --env production
+directory=/home/orchardnest/app
+user=orchardnest
+environment=DATABASE_URL='postgres://orchardnest:12345@localhost/orchardnest'
+stdout_logfile=/var/log/supervisor/%(program_name)-stdout.log
+stderr_logfile=/var/log/supervisor/%(program_name)-stderr.log
diff --git a/Documentation/Reference/classes/BlogReader.md b/Documentation/Reference/classes/BlogReader.md
deleted file mode 100644
index caf0e5b..0000000
--- a/Documentation/Reference/classes/BlogReader.md
+++ /dev/null
@@ -1,20 +0,0 @@
-**CLASS**
-
-# `BlogReader`
-
-```swift
-public class BlogReader
-```
-
-## Methods
-### `init()`
-
-```swift
-public init()
-```
-
-### `sites(fromURL:)`
-
-```swift
-public func sites(fromURL url: URL) throws -> [LanguageContent]
-```
diff --git a/Documentation/Reference/enums/EntryCategory.md b/Documentation/Reference/enums/EntryCategory.md
new file mode 100644
index 0000000..ef6059f
--- /dev/null
+++ b/Documentation/Reference/enums/EntryCategory.md
@@ -0,0 +1,106 @@
+**ENUM**
+
+# `EntryCategory`
+
+```swift
+public enum EntryCategory: Codable
+```
+
+## Cases
+### `companies`
+
+```swift
+case companies
+```
+
+### `design`
+
+```swift
+case design
+```
+
+### `development`
+
+```swift
+case development
+```
+
+### `marketing`
+
+```swift
+case marketing
+```
+
+### `newsletters`
+
+```swift
+case newsletters
+```
+
+### `podcasts(_:)`
+
+```swift
+case podcasts(URL)
+```
+
+### `updates`
+
+```swift
+case updates
+```
+
+### `youtube(_:)`
+
+```swift
+case youtube(String)
+```
+
+## Properties
+### `type`
+
+```swift
+public var type: EntryCategoryType
+```
+
+## Methods
+### `init(podcastEpisodeAtURL:)`
+
+```swift
+public init(podcastEpisodeAtURL url: URL)
+```
+
+### `init(youtubeVideoWithID:)`
+
+```swift
+public init(youtubeVideoWithID id: String)
+```
+
+### `init(type:)`
+
+```swift
+public init(type: EntryCategoryType) throws
+```
+
+### `init(from:)`
+
+```swift
+public init(from decoder: Decoder) throws
+```
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| decoder | The decoder to read data from. |
+
+### `encode(to:)`
+
+```swift
+public func encode(to encoder: Encoder) throws
+```
+
+#### Parameters
+
+| Name | Description |
+| ---- | ----------- |
+| encoder | The encoder to write data to. |
\ No newline at end of file
diff --git a/Documentation/Reference/enums/EntryCategoryType.md b/Documentation/Reference/enums/EntryCategoryType.md
new file mode 100644
index 0000000..aaabe24
--- /dev/null
+++ b/Documentation/Reference/enums/EntryCategoryType.md
@@ -0,0 +1,56 @@
+**ENUM**
+
+# `EntryCategoryType`
+
+```swift
+public enum EntryCategoryType: String, Codable
+```
+
+## Cases
+### `companies`
+
+```swift
+case companies
+```
+
+### `design`
+
+```swift
+case design
+```
+
+### `development`
+
+```swift
+case development
+```
+
+### `marketing`
+
+```swift
+case marketing
+```
+
+### `newsletters`
+
+```swift
+case newsletters
+```
+
+### `podcasts`
+
+```swift
+case podcasts
+```
+
+### `updates`
+
+```swift
+case updates
+```
+
+### `youtube`
+
+```swift
+case youtube
+```
diff --git a/Documentation/Reference/extensions/EntryItem.md b/Documentation/Reference/extensions/EntryItem.md
new file mode 100644
index 0000000..f7d74d2
--- /dev/null
+++ b/Documentation/Reference/extensions/EntryItem.md
@@ -0,0 +1,25 @@
+**EXTENSION**
+
+# `EntryItem`
+```swift
+public extension EntryItem
+```
+
+## Properties
+### `podcastEpisodeURL`
+
+```swift
+var podcastEpisodeURL: URL?
+```
+
+### `youtubeID`
+
+```swift
+var youtubeID: String?
+```
+
+### `twitterShareLink`
+
+```swift
+var twitterShareLink: String
+```
diff --git a/Documentation/Reference/structs/EntryChannel.md b/Documentation/Reference/structs/EntryChannel.md
new file mode 100644
index 0000000..6bd4a4a
--- /dev/null
+++ b/Documentation/Reference/structs/EntryChannel.md
@@ -0,0 +1,65 @@
+**STRUCT**
+
+# `EntryChannel`
+
+```swift
+public struct EntryChannel: Codable
+```
+
+## Properties
+### `id`
+
+```swift
+public let id: UUID
+```
+
+### `title`
+
+```swift
+public let title: String
+```
+
+### `author`
+
+```swift
+public let author: String
+```
+
+### `siteURL`
+
+```swift
+public let siteURL: URL
+```
+
+### `twitterHandle`
+
+```swift
+public let twitterHandle: String?
+```
+
+### `imageURL`
+
+```swift
+public let imageURL: URL?
+```
+
+### `podcastAppleId`
+
+```swift
+public let podcastAppleId: Int?
+```
+
+## Methods
+### `init(id:title:siteURL:author:twitterHandle:imageURL:podcastAppleId:)`
+
+```swift
+public init(
+ id: UUID,
+ title: String,
+ siteURL: URL,
+ author: String,
+ twitterHandle: String?,
+ imageURL: URL?,
+ podcastAppleId: Int?
+)
+```
diff --git a/Documentation/Reference/structs/EntryItem.md b/Documentation/Reference/structs/EntryItem.md
new file mode 100644
index 0000000..faa5664
--- /dev/null
+++ b/Documentation/Reference/structs/EntryItem.md
@@ -0,0 +1,77 @@
+**STRUCT**
+
+# `EntryItem`
+
+```swift
+public struct EntryItem: Codable
+```
+
+## Properties
+### `id`
+
+```swift
+public let id: UUID
+```
+
+### `channel`
+
+```swift
+public let channel: EntryChannel
+```
+
+### `feedId`
+
+```swift
+public let feedId: String
+```
+
+### `title`
+
+```swift
+public let title: String
+```
+
+### `summary`
+
+```swift
+public let summary: String
+```
+
+### `url`
+
+```swift
+public let url: URL
+```
+
+### `imageURL`
+
+```swift
+public let imageURL: URL?
+```
+
+### `publishedAt`
+
+```swift
+public let publishedAt: Date
+```
+
+### `category`
+
+```swift
+public let category: EntryCategory
+```
+
+## Methods
+### `init(id:channel:category:feedId:title:summary:url:imageURL:publishedAt:)`
+
+```swift
+public init(id: UUID,
+ channel: EntryChannel,
+ category: EntryCategory,
+ feedId: String,
+ title: String,
+ summary: String,
+ url: URL,
+ imageURL: URL?,
+ publishedAt: Date)
+```
diff --git a/Documentation/Reference/structs/Channel.md b/Documentation/Reference/structs/FeedChannel.md
similarity index 81%
rename from Documentation/Reference/structs/Channel.md
rename to Documentation/Reference/structs/FeedChannel.md
index f9378c5..38ef056 100644
--- a/Documentation/Reference/structs/Channel.md
+++ b/Documentation/Reference/structs/FeedChannel.md
@@ -1,9 +1,9 @@
**STRUCT**
-# `Channel`
+# `FeedChannel`
```swift
-public struct Channel: Codable
+public struct FeedChannel: Codable
```
## Properties
@@ -76,7 +76,7 @@ public let category: String
### `items`
```swift
-public let items: [Item]
+public let items: [FeedItem]
```
### `itemCount`
@@ -92,8 +92,8 @@ public let itemCount: Int?
public static func imageURL(fromYoutubeId ytId: String) -> URL
```
-### `init(language:category:site:)`
+### `init(language:category:site:data:)`
```swift
-public init(language: String, category: String, site: Site) throws
+public init(language: String, category: String, site: Site, data: Data) throws
```
diff --git a/Documentation/Reference/structs/Item.md b/Documentation/Reference/structs/FeedItem.md
similarity index 92%
rename from Documentation/Reference/structs/Item.md
rename to Documentation/Reference/structs/FeedItem.md
index 32da125..16e6849 100644
--- a/Documentation/Reference/structs/Item.md
+++ b/Documentation/Reference/structs/FeedItem.md
@@ -1,9 +1,9 @@
**STRUCT**
-# `Item`
+# `FeedItem`
```swift
-public struct Item: Codable
+public struct FeedItem: Codable
```
## Properties
diff --git a/Package.swift b/Package.swift
index 08f238f..ab623da 100644
--- a/Package.swift
+++ b/Package.swift
@@ -16,18 +16,19 @@ let package = Package(
name: "OrchardNestServer",
targets: ["OrchardNestServer"]
),
- .executable(name: "orcnst-serve", targets: ["orcnst-serve"]),
- .executable(name: "orcnst", targets: ["orcnst"])
+ .executable(name: "orchardnestd", targets: ["orchardnestd"])
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/brightdigit/FeedKit.git", .branch("master")),
.package(url: "https://github.com/shibapm/Komondor", from: "1.0.5"),
- .package(url: "https://github.com/eneko/SourceDocs", from: "1.0.0"),
+ .package(url: "https://github.com/eneko/SourceDocs", from: "1.2.1"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
- .package(name: "QueuesFluentDriver", url: "https://github.com/m-barthelemy/vapor-queues-fluent-driver.git", from: "0.3.8")
+ .package(name: "QueuesFluentDriver", url: "https://github.com/m-barthelemy/vapor-queues-fluent-driver.git", from: "0.3.8"),
+ .package(name: "Plot", url: "https://github.com/johnsundell/plot.git", from: "0.8.0"),
+ .package(url: "https://github.com/JohnSundell/Ink.git", from: "0.1.0")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
@@ -42,12 +43,12 @@ let package = Package(
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
.product(name: "Vapor", package: "vapor"),
- .product(name: "QueuesFluentDriver", package: "QueuesFluentDriver")]
+ .product(name: "QueuesFluentDriver", package: "QueuesFluentDriver"),
+ .product(name: "Plot", package: "Plot"),
+ .product(name: "Ink", package: "Ink")]
),
- .target(name: "orcnst",
- dependencies: ["OrchardNestKit", "FeedKit"]),
- .target(name: "orcnst-serve",
- dependencies: ["OrchardNestKit", "OrchardNestServer"]),
+ .target(name: "orchardnestd",
+ dependencies: ["OrchardNestKit", "OrchardNestServer", "FeedKit"]),
.testTarget(
name: "OrchardNestKitTests",
dependencies: ["OrchardNestKit"]
@@ -65,7 +66,7 @@ let package = Package(
"swift test --enable-code-coverage --enable-test-discovery --generate-linuxmain",
"swift run swiftformat .",
"swift run swiftlint autocorrect",
- "swift run sourcedocs generate --spm-module OrchardNest -r",
+ "swift run sourcedocs generate --spm-module OrchardNestKit -c -r",
// "swift run swiftpmls mine",
"git add .",
"swift run swiftformat --lint .",
diff --git a/Public/android-chrome-192x192.png b/Public/android-chrome-192x192.png
new file mode 100644
index 0000000..b08deb9
Binary files /dev/null and b/Public/android-chrome-192x192.png differ
diff --git a/Public/android-chrome-512x512.png b/Public/android-chrome-512x512.png
new file mode 100644
index 0000000..12e6a48
Binary files /dev/null and b/Public/android-chrome-512x512.png differ
diff --git a/Public/apple-touch-icon.png b/Public/apple-touch-icon.png
new file mode 100644
index 0000000..a1cba81
Binary files /dev/null and b/Public/apple-touch-icon.png differ
diff --git a/Public/browserconfig.xml b/Public/browserconfig.xml
new file mode 100644
index 0000000..f9c2e67
--- /dev/null
+++ b/Public/browserconfig.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+ #2b5797
+
+
+
diff --git a/Public/favicon-16x16.png b/Public/favicon-16x16.png
new file mode 100644
index 0000000..fc45fd3
Binary files /dev/null and b/Public/favicon-16x16.png differ
diff --git a/Public/favicon-32x32.png b/Public/favicon-32x32.png
new file mode 100644
index 0000000..fc3facb
Binary files /dev/null and b/Public/favicon-32x32.png differ
diff --git a/Public/favicon.ico b/Public/favicon.ico
new file mode 100644
index 0000000..4ed150a
Binary files /dev/null and b/Public/favicon.ico differ
diff --git a/Public/images/logo.png b/Public/images/logo.png
new file mode 100644
index 0000000..f074929
Binary files /dev/null and b/Public/images/logo.png differ
diff --git a/Public/images/logo.svg b/Public/images/logo.svg
new file mode 100644
index 0000000..6b103df
--- /dev/null
+++ b/Public/images/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Public/images/podcast-players/apple/badge.svg b/Public/images/podcast-players/apple/badge.svg
new file mode 100755
index 0000000..6989219
--- /dev/null
+++ b/Public/images/podcast-players/apple/badge.svg
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public/images/podcast-players/apple/icon.png b/Public/images/podcast-players/apple/icon.png
new file mode 100644
index 0000000..39eba4d
Binary files /dev/null and b/Public/images/podcast-players/apple/icon.png differ
diff --git a/Public/images/podcast-players/apple/icon.svg b/Public/images/podcast-players/apple/icon.svg
new file mode 100644
index 0000000..e7248b7
--- /dev/null
+++ b/Public/images/podcast-players/apple/icon.svg
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public/images/podcast-players/castro/badge.svg b/Public/images/podcast-players/castro/badge.svg
new file mode 100644
index 0000000..8ab53cd
--- /dev/null
+++ b/Public/images/podcast-players/castro/badge.svg
@@ -0,0 +1,47 @@
+
+
+
+ Open in Castro
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Public/images/podcast-players/castro/icon.svg b/Public/images/podcast-players/castro/icon.svg
new file mode 100644
index 0000000..6176ed1
--- /dev/null
+++ b/Public/images/podcast-players/castro/icon.svg
@@ -0,0 +1,23 @@
+
+
+
+ c2icon copy
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Public/images/podcast-players/overcast/icon.png b/Public/images/podcast-players/overcast/icon.png
new file mode 100644
index 0000000..ed489d9
Binary files /dev/null and b/Public/images/podcast-players/overcast/icon.png differ
diff --git a/Public/images/podcast-players/overcast/icon.svg b/Public/images/podcast-players/overcast/icon.svg
new file mode 100644
index 0000000..ae01e12
--- /dev/null
+++ b/Public/images/podcast-players/overcast/icon.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/Public/images/podcast-players/pocketcasts/badge.svg b/Public/images/podcast-players/pocketcasts/badge.svg
new file mode 100644
index 0000000..5683099
--- /dev/null
+++ b/Public/images/podcast-players/pocketcasts/badge.svg
@@ -0,0 +1,40 @@
+
+
+
+ Badges/pocketcasts_medium_light
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Public/images/podcast-players/pocketcasts/icon.svg b/Public/images/podcast-players/pocketcasts/icon.svg
new file mode 100644
index 0000000..4413310
--- /dev/null
+++ b/Public/images/podcast-players/pocketcasts/icon.svg
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public/mstile-150x150.png b/Public/mstile-150x150.png
new file mode 100644
index 0000000..2802db1
Binary files /dev/null and b/Public/mstile-150x150.png differ
diff --git a/Public/safari-pinned-tab.svg b/Public/safari-pinned-tab.svg
new file mode 100644
index 0000000..ceb5027
--- /dev/null
+++ b/Public/safari-pinned-tab.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Public/site.webmanifest b/Public/site.webmanifest
new file mode 100644
index 0000000..b20abb7
--- /dev/null
+++ b/Public/site.webmanifest
@@ -0,0 +1,19 @@
+{
+ "name": "",
+ "short_name": "",
+ "icons": [
+ {
+ "src": "/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
diff --git a/Public/styles/elusive-icons/css/elusive-icons.css b/Public/styles/elusive-icons/css/elusive-icons.css
new file mode 100644
index 0000000..b6db7c2
--- /dev/null
+++ b/Public/styles/elusive-icons/css/elusive-icons.css
@@ -0,0 +1,1082 @@
+/*!
+ * Elusive Icons 2.0.0 by @ReduxFramework - http://elusiveicons.com - @reduxframework
+ * License - http://elusiveicons.com/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */
+/* FONT PATH
+ * -------------------------- */
+@font-face {
+ font-family: 'Elusive-Icons';
+ src: url('../fonts/elusiveicons-webfont.eot?v=2.0.0');
+ src: url('../fonts/elusiveicons-webfont.eot?#iefix&v=2.0.0') format('embedded-opentype'), url('../fonts/elusiveicons-webfont.woff?v=2.0.0') format('woff'), url('../fonts/elusiveicons-webfont.ttf?v=2.0.0') format('truetype'), url('../fonts/elusiveicons-webfont.svg?v=2.0.0#elusiveiconsregular') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+.el {
+ display: inline-block;
+ font: normal normal normal 14px/1 'Elusive-Icons';
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ transform: translate(0, 0);
+}
+/* makes the font 33% larger relative to the icon container */
+.el-lg {
+ font-size: 1.33333333em;
+ line-height: 0.75em;
+ vertical-align: -15%;
+}
+.el-2x {
+ font-size: 2em;
+}
+.el-3x {
+ font-size: 3em;
+}
+.el-4x {
+ font-size: 4em;
+}
+.el-5x {
+ font-size: 5em;
+}
+.el-fw {
+ width: 1.28571429em;
+ text-align: center;
+}
+.el-ul {
+ padding-left: 0;
+ margin-left: 2.14285714em;
+ list-style-type: none;
+}
+.el-ul > li {
+ position: relative;
+}
+.el-li {
+ position: absolute;
+ left: -2.14285714em;
+ width: 2.14285714em;
+ top: 0.14285714em;
+ text-align: center;
+}
+.el-li.el-lg {
+ left: -1.85714286em;
+}
+.el-border {
+ padding: .2em .25em .15em;
+ border: solid 0.08em #eeeeee;
+ border-radius: .1em;
+}
+.pull-right {
+ float: right;
+}
+.pull-left {
+ float: left;
+}
+.el.pull-left {
+ margin-right: .3em;
+}
+.el.pull-right {
+ margin-left: .3em;
+}
+.el-spin {
+ -webkit-animation: el-spin 2s infinite linear;
+ animation: el-spin 2s infinite linear;
+}
+.el-pulse {
+ -webkit-animation: el-spin 1s infinite steps(8);
+ animation: el-spin 1s infinite steps(8);
+}
+@-webkit-keyframes el-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@keyframes el-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+.el-rotate-90 {
+ filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1);
+ -webkit-transform: rotate(90deg);
+ -ms-transform: rotate(90deg);
+ transform: rotate(90deg);
+}
+.el-rotate-180 {
+ filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2);
+ -webkit-transform: rotate(180deg);
+ -ms-transform: rotate(180deg);
+ transform: rotate(180deg);
+}
+.el-rotate-270 {
+ filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3);
+ -webkit-transform: rotate(270deg);
+ -ms-transform: rotate(270deg);
+ transform: rotate(270deg);
+}
+.el-flip-horizontal {
+ filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);
+ -webkit-transform: scale(-1, 1);
+ -ms-transform: scale(-1, 1);
+ transform: scale(-1, 1);
+}
+.el-flip-vertical {
+ filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);
+ -webkit-transform: scale(1, -1);
+ -ms-transform: scale(1, -1);
+ transform: scale(1, -1);
+}
+:root .el-rotate-90,
+:root .el-rotate-180,
+:root .el-rotate-270,
+:root .el-flip-horizontal,
+:root .el-flip-vertical {
+ filter: none;
+}
+.el-stack {
+ position: relative;
+ display: inline-block;
+ width: 2em;
+ height: 2em;
+ line-height: 2em;
+ vertical-align: middle;
+}
+.el-stack-1x,
+.el-stack-2x {
+ position: absolute;
+ left: 0;
+ width: 100%;
+ text-align: center;
+}
+.el-stack-1x {
+ line-height: inherit;
+}
+.el-stack-2x {
+ font-size: 2em;
+}
+.el-inverse {
+ color: #ffffff;
+}
+/* Elusive Icons uses the Unicode Private Use Area (PUA) to ensure screen
+ readers do not read off random characters that represent icons */
+.el-address-book-alt:before {
+ content: "\f101";
+}
+.el-address-book:before {
+ content: "\f102";
+}
+.el-adjust-alt:before {
+ content: "\f103";
+}
+.el-adjust:before {
+ content: "\f104";
+}
+.el-adult:before {
+ content: "\f105";
+}
+.el-align-center:before {
+ content: "\f106";
+}
+.el-align-justify:before {
+ content: "\f107";
+}
+.el-align-left:before {
+ content: "\f108";
+}
+.el-align-right:before {
+ content: "\f109";
+}
+.el-arrow-down:before {
+ content: "\f10a";
+}
+.el-arrow-left:before {
+ content: "\f10b";
+}
+.el-arrow-right:before {
+ content: "\f10c";
+}
+.el-arrow-up:before {
+ content: "\f10d";
+}
+.el-asl:before {
+ content: "\f10e";
+}
+.el-asterisk:before {
+ content: "\f10f";
+}
+.el-backward:before {
+ content: "\f110";
+}
+.el-ban-circle:before {
+ content: "\f111";
+}
+.el-barcode:before {
+ content: "\f112";
+}
+.el-behance:before {
+ content: "\f113";
+}
+.el-bell:before {
+ content: "\f114";
+}
+.el-blind:before {
+ content: "\f115";
+}
+.el-blogger:before {
+ content: "\f116";
+}
+.el-bold:before {
+ content: "\f117";
+}
+.el-book:before {
+ content: "\f118";
+}
+.el-bookmark-empty:before {
+ content: "\f119";
+}
+.el-bookmark:before {
+ content: "\f11a";
+}
+.el-braille:before {
+ content: "\f11b";
+}
+.el-briefcase:before {
+ content: "\f11c";
+}
+.el-broom:before {
+ content: "\f11d";
+}
+.el-brush:before {
+ content: "\f11e";
+}
+.el-bulb:before {
+ content: "\f11f";
+}
+.el-bullhorn:before {
+ content: "\f120";
+}
+.el-calendar-sign:before {
+ content: "\f121";
+}
+.el-calendar:before {
+ content: "\f122";
+}
+.el-camera:before {
+ content: "\f123";
+}
+.el-car:before {
+ content: "\f124";
+}
+.el-caret-down:before {
+ content: "\f125";
+}
+.el-caret-left:before {
+ content: "\f126";
+}
+.el-caret-right:before {
+ content: "\f127";
+}
+.el-caret-up:before {
+ content: "\f128";
+}
+.el-cc:before {
+ content: "\f129";
+}
+.el-certificate:before {
+ content: "\f12a";
+}
+.el-check-empty:before {
+ content: "\f12b";
+}
+.el-check:before {
+ content: "\f12c";
+}
+.el-chevron-down:before {
+ content: "\f12d";
+}
+.el-chevron-left:before {
+ content: "\f12e";
+}
+.el-chevron-right:before {
+ content: "\f12f";
+}
+.el-chevron-up:before {
+ content: "\f130";
+}
+.el-child:before {
+ content: "\f131";
+}
+.el-circle-arrow-down:before {
+ content: "\f132";
+}
+.el-circle-arrow-left:before {
+ content: "\f133";
+}
+.el-circle-arrow-right:before {
+ content: "\f134";
+}
+.el-circle-arrow-up:before {
+ content: "\f135";
+}
+.el-cloud-alt:before {
+ content: "\f136";
+}
+.el-cloud:before {
+ content: "\f137";
+}
+.el-cog-alt:before {
+ content: "\f138";
+}
+.el-cog:before {
+ content: "\f139";
+}
+.el-cogs:before {
+ content: "\f13a";
+}
+.el-comment-alt:before {
+ content: "\f13b";
+}
+.el-comment:before {
+ content: "\f13c";
+}
+.el-compass-alt:before {
+ content: "\f13d";
+}
+.el-compass:before {
+ content: "\f13e";
+}
+.el-credit-card:before {
+ content: "\f13f";
+}
+.el-css:before {
+ content: "\f140";
+}
+.el-dashboard:before {
+ content: "\f141";
+}
+.el-delicious:before {
+ content: "\f142";
+}
+.el-deviantart:before {
+ content: "\f143";
+}
+.el-digg:before {
+ content: "\f144";
+}
+.el-download-alt:before {
+ content: "\f145";
+}
+.el-download:before {
+ content: "\f146";
+}
+.el-dribbble:before {
+ content: "\f147";
+}
+.el-edit:before {
+ content: "\f148";
+}
+.el-eject:before {
+ content: "\f149";
+}
+.el-envelope-alt:before {
+ content: "\f14a";
+}
+.el-envelope:before {
+ content: "\f14b";
+}
+.el-error-alt:before {
+ content: "\f14c";
+}
+.el-error:before {
+ content: "\f14d";
+}
+.el-eur:before {
+ content: "\f14e";
+}
+.el-exclamation-sign:before {
+ content: "\f14f";
+}
+.el-eye-close:before {
+ content: "\f150";
+}
+.el-eye-open:before {
+ content: "\f151";
+}
+.el-facebook:before {
+ content: "\f152";
+}
+.el-facetime-video:before {
+ content: "\f153";
+}
+.el-fast-backward:before {
+ content: "\f154";
+}
+.el-fast-forward:before {
+ content: "\f155";
+}
+.el-female:before {
+ content: "\f156";
+}
+.el-file-alt:before {
+ content: "\f157";
+}
+.el-file-edit-alt:before {
+ content: "\f158";
+}
+.el-file-edit:before {
+ content: "\f159";
+}
+.el-file-new-alt:before {
+ content: "\f15a";
+}
+.el-file-new:before {
+ content: "\f15b";
+}
+.el-file:before {
+ content: "\f15c";
+}
+.el-film:before {
+ content: "\f15d";
+}
+.el-filter:before {
+ content: "\f15e";
+}
+.el-fire:before {
+ content: "\f15f";
+}
+.el-flag-alt:before {
+ content: "\f160";
+}
+.el-flag:before {
+ content: "\f161";
+}
+.el-flickr:before {
+ content: "\f162";
+}
+.el-folder-close:before {
+ content: "\f163";
+}
+.el-folder-open:before {
+ content: "\f164";
+}
+.el-folder-sign:before {
+ content: "\f165";
+}
+.el-folder:before {
+ content: "\f166";
+}
+.el-font:before {
+ content: "\f167";
+}
+.el-fontsize:before {
+ content: "\f168";
+}
+.el-fork:before {
+ content: "\f169";
+}
+.el-forward-alt:before {
+ content: "\f16a";
+}
+.el-forward:before {
+ content: "\f16b";
+}
+.el-foursquare:before {
+ content: "\f16c";
+}
+.el-friendfeed-rect:before {
+ content: "\f16d";
+}
+.el-friendfeed:before {
+ content: "\f16e";
+}
+.el-fullscreen:before {
+ content: "\f16f";
+}
+.el-gbp:before {
+ content: "\f170";
+}
+.el-gift:before {
+ content: "\f171";
+}
+.el-github-text:before {
+ content: "\f172";
+}
+.el-github:before {
+ content: "\f173";
+}
+.el-glass:before {
+ content: "\f174";
+}
+.el-glasses:before {
+ content: "\f175";
+}
+.el-globe-alt:before {
+ content: "\f176";
+}
+.el-globe:before {
+ content: "\f177";
+}
+.el-googleplus:before {
+ content: "\f178";
+}
+.el-graph-alt:before {
+ content: "\f179";
+}
+.el-graph:before {
+ content: "\f17a";
+}
+.el-group-alt:before {
+ content: "\f17b";
+}
+.el-group:before {
+ content: "\f17c";
+}
+.el-guidedog:before {
+ content: "\f17d";
+}
+.el-hand-down:before {
+ content: "\f17e";
+}
+.el-hand-left:before {
+ content: "\f17f";
+}
+.el-hand-right:before {
+ content: "\f180";
+}
+.el-hand-up:before {
+ content: "\f181";
+}
+.el-hdd:before {
+ content: "\f182";
+}
+.el-headphones:before {
+ content: "\f183";
+}
+.el-hearing-impaired:before {
+ content: "\f184";
+}
+.el-heart-alt:before {
+ content: "\f185";
+}
+.el-heart-empty:before {
+ content: "\f186";
+}
+.el-heart:before {
+ content: "\f187";
+}
+.el-home-alt:before {
+ content: "\f188";
+}
+.el-home:before {
+ content: "\f189";
+}
+.el-hourglass:before {
+ content: "\f18a";
+}
+.el-idea-alt:before {
+ content: "\f18b";
+}
+.el-idea:before {
+ content: "\f18c";
+}
+.el-inbox-alt:before {
+ content: "\f18d";
+}
+.el-inbox-box:before {
+ content: "\f18e";
+}
+.el-inbox:before {
+ content: "\f18f";
+}
+.el-indent-left:before {
+ content: "\f190";
+}
+.el-indent-right:before {
+ content: "\f191";
+}
+.el-info-circle:before {
+ content: "\f192";
+}
+.el-instagram:before {
+ content: "\f193";
+}
+.el-iphone-home:before {
+ content: "\f194";
+}
+.el-italic:before {
+ content: "\f195";
+}
+.el-key:before {
+ content: "\f196";
+}
+.el-laptop-alt:before {
+ content: "\f197";
+}
+.el-laptop:before {
+ content: "\f198";
+}
+.el-lastfm:before {
+ content: "\f199";
+}
+.el-leaf:before {
+ content: "\f19a";
+}
+.el-lines:before {
+ content: "\f19b";
+}
+.el-link:before {
+ content: "\f19c";
+}
+.el-linkedin:before {
+ content: "\f19d";
+}
+.el-list-alt:before {
+ content: "\f19e";
+}
+.el-list:before {
+ content: "\f19f";
+}
+.el-livejournal:before {
+ content: "\f1a0";
+}
+.el-lock-alt:before {
+ content: "\f1a1";
+}
+.el-lock:before {
+ content: "\f1a2";
+}
+.el-magic:before {
+ content: "\f1a3";
+}
+.el-magnet:before {
+ content: "\f1a4";
+}
+.el-male:before {
+ content: "\f1a5";
+}
+.el-map-marker-alt:before {
+ content: "\f1a6";
+}
+.el-map-marker:before {
+ content: "\f1a7";
+}
+.el-mic-alt:before {
+ content: "\f1a8";
+}
+.el-mic:before {
+ content: "\f1a9";
+}
+.el-minus-sign:before {
+ content: "\f1aa";
+}
+.el-minus:before {
+ content: "\f1ab";
+}
+.el-move:before {
+ content: "\f1ac";
+}
+.el-music:before {
+ content: "\f1ad";
+}
+.el-myspace:before {
+ content: "\f1ae";
+}
+.el-network:before {
+ content: "\f1af";
+}
+.el-off:before {
+ content: "\f1b0";
+}
+.el-ok-circle:before {
+ content: "\f1b1";
+}
+.el-ok-sign:before {
+ content: "\f1b2";
+}
+.el-ok:before {
+ content: "\f1b3";
+}
+.el-opensource:before {
+ content: "\f1b4";
+}
+.el-paper-clip-alt:before {
+ content: "\f1b5";
+}
+.el-paper-clip:before {
+ content: "\f1b6";
+}
+.el-path:before {
+ content: "\f1b7";
+}
+.el-pause-alt:before {
+ content: "\f1b8";
+}
+.el-pause:before {
+ content: "\f1b9";
+}
+.el-pencil-alt:before {
+ content: "\f1ba";
+}
+.el-pencil:before {
+ content: "\f1bb";
+}
+.el-person:before {
+ content: "\f1bc";
+}
+.el-phone-alt:before {
+ content: "\f1bd";
+}
+.el-phone:before {
+ content: "\f1be";
+}
+.el-photo-alt:before {
+ content: "\f1bf";
+}
+.el-photo:before {
+ content: "\f1c0";
+}
+.el-picasa:before {
+ content: "\f1c1";
+}
+.el-picture:before {
+ content: "\f1c2";
+}
+.el-pinterest:before {
+ content: "\f1c3";
+}
+.el-plane:before {
+ content: "\f1c4";
+}
+.el-play-alt:before {
+ content: "\f1c5";
+}
+.el-play-circle:before {
+ content: "\f1c6";
+}
+.el-play:before {
+ content: "\f1c7";
+}
+.el-plurk-alt:before {
+ content: "\f1c8";
+}
+.el-plurk:before {
+ content: "\f1c9";
+}
+.el-plus-sign:before {
+ content: "\f1ca";
+}
+.el-plus:before {
+ content: "\f1cb";
+}
+.el-podcast:before {
+ content: "\f1cc";
+}
+.el-print:before {
+ content: "\f1cd";
+}
+.el-puzzle:before {
+ content: "\f1ce";
+}
+.el-qrcode:before {
+ content: "\f1cf";
+}
+.el-question-sign:before {
+ content: "\f1d0";
+}
+.el-question:before {
+ content: "\f1d1";
+}
+.el-quote-alt:before {
+ content: "\f1d2";
+}
+.el-quote-right-alt:before {
+ content: "\f1d3";
+}
+.el-quote-right:before {
+ content: "\f1d4";
+}
+.el-quotes:before {
+ content: "\f1d5";
+}
+.el-random:before {
+ content: "\f1d6";
+}
+.el-record:before {
+ content: "\f1d7";
+}
+.el-reddit:before {
+ content: "\f1d8";
+}
+.el-redux:before {
+ content: "\f1d9";
+}
+.el-refresh:before {
+ content: "\f1da";
+}
+.el-remove-circle:before {
+ content: "\f1db";
+}
+.el-remove-sign:before {
+ content: "\f1dc";
+}
+.el-remove:before {
+ content: "\f1dd";
+}
+.el-repeat-alt:before {
+ content: "\f1de";
+}
+.el-repeat:before {
+ content: "\f1df";
+}
+.el-resize-full:before {
+ content: "\f1e0";
+}
+.el-resize-horizontal:before {
+ content: "\f1e1";
+}
+.el-resize-small:before {
+ content: "\f1e2";
+}
+.el-resize-vertical:before {
+ content: "\f1e3";
+}
+.el-return-key:before {
+ content: "\f1e4";
+}
+.el-retweet:before {
+ content: "\f1e5";
+}
+.el-reverse-alt:before {
+ content: "\f1e6";
+}
+.el-road:before {
+ content: "\f1e7";
+}
+.el-rss:before {
+ content: "\f1e8";
+}
+.el-scissors:before {
+ content: "\f1e9";
+}
+.el-screen-alt:before {
+ content: "\f1ea";
+}
+.el-screen:before {
+ content: "\f1eb";
+}
+.el-screenshot:before {
+ content: "\f1ec";
+}
+.el-search-alt:before {
+ content: "\f1ed";
+}
+.el-search:before {
+ content: "\f1ee";
+}
+.el-share-alt:before {
+ content: "\f1ef";
+}
+.el-share:before {
+ content: "\f1f0";
+}
+.el-shopping-cart-sign:before {
+ content: "\f1f1";
+}
+.el-shopping-cart:before {
+ content: "\f1f2";
+}
+.el-signal:before {
+ content: "\f1f3";
+}
+.el-skype:before {
+ content: "\f1f4";
+}
+.el-slideshare:before {
+ content: "\f1f5";
+}
+.el-smiley-alt:before {
+ content: "\f1f6";
+}
+.el-smiley:before {
+ content: "\f1f7";
+}
+.el-soundcloud:before {
+ content: "\f1f8";
+}
+.el-speaker:before {
+ content: "\f1f9";
+}
+.el-spotify:before {
+ content: "\f1fa";
+}
+.el-stackoverflow:before {
+ content: "\f1fb";
+}
+.el-star-alt:before {
+ content: "\f1fc";
+}
+.el-star-empty:before {
+ content: "\f1fd";
+}
+.el-star:before {
+ content: "\f1fe";
+}
+.el-step-backward:before {
+ content: "\f1ff";
+}
+.el-step-forward:before {
+ content: "\f200";
+}
+.el-stop-alt:before {
+ content: "\f201";
+}
+.el-stop:before {
+ content: "\f202";
+}
+.el-stumbleupon:before {
+ content: "\f203";
+}
+.el-tag:before {
+ content: "\f204";
+}
+.el-tags:before {
+ content: "\f205";
+}
+.el-tasks:before {
+ content: "\f206";
+}
+.el-text-height:before {
+ content: "\f207";
+}
+.el-text-width:before {
+ content: "\f208";
+}
+.el-th-large:before {
+ content: "\f209";
+}
+.el-th-list:before {
+ content: "\f20a";
+}
+.el-th:before {
+ content: "\f20b";
+}
+.el-thumbs-down:before {
+ content: "\f20c";
+}
+.el-thumbs-up:before {
+ content: "\f20d";
+}
+.el-time-alt:before {
+ content: "\f20e";
+}
+.el-time:before {
+ content: "\f20f";
+}
+.el-tint:before {
+ content: "\f210";
+}
+.el-torso:before {
+ content: "\f211";
+}
+.el-trash-alt:before {
+ content: "\f212";
+}
+.el-trash:before {
+ content: "\f213";
+}
+.el-tumblr:before {
+ content: "\f214";
+}
+.el-twitter:before {
+ content: "\f215";
+}
+.el-universal-access:before {
+ content: "\f216";
+}
+.el-unlock-alt:before {
+ content: "\f217";
+}
+.el-unlock:before {
+ content: "\f218";
+}
+.el-upload:before {
+ content: "\f219";
+}
+.el-usd:before {
+ content: "\f21a";
+}
+.el-user:before {
+ content: "\f21b";
+}
+.el-viadeo:before {
+ content: "\f21c";
+}
+.el-video-alt:before {
+ content: "\f21d";
+}
+.el-video-chat:before {
+ content: "\f21e";
+}
+.el-video:before {
+ content: "\f21f";
+}
+.el-view-mode:before {
+ content: "\f220";
+}
+.el-vimeo:before {
+ content: "\f221";
+}
+.el-vkontakte:before {
+ content: "\f222";
+}
+.el-volume-down:before {
+ content: "\f223";
+}
+.el-volume-off:before {
+ content: "\f224";
+}
+.el-volume-up:before {
+ content: "\f225";
+}
+.el-w3c:before {
+ content: "\f226";
+}
+.el-warning-sign:before {
+ content: "\f227";
+}
+.el-website-alt:before {
+ content: "\f228";
+}
+.el-website:before {
+ content: "\f229";
+}
+.el-wheelchair:before {
+ content: "\f22a";
+}
+.el-wordpress:before {
+ content: "\f22b";
+}
+.el-wrench-alt:before {
+ content: "\f22c";
+}
+.el-wrench:before {
+ content: "\f22d";
+}
+.el-youtube:before {
+ content: "\f22e";
+}
+.el-zoom-in:before {
+ content: "\f22f";
+}
+.el-zoom-out:before {
+ content: "\f230";
+}
diff --git a/Public/styles/elusive-icons/css/elusive-icons.min.css b/Public/styles/elusive-icons/css/elusive-icons.min.css
new file mode 100644
index 0000000..f053926
--- /dev/null
+++ b/Public/styles/elusive-icons/css/elusive-icons.min.css
@@ -0,0 +1,4 @@
+/*!
+ * Elusive Icons 2.0.0 by @ReduxFramework - http://elusiveicons.com - @reduxframework
+ * License - http://elusiveicons.com/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */@font-face{font-family:'Elusive-Icons';src:url('../fonts/elusiveicons-webfont.eot?v=2.0.0');src:url('../fonts/elusiveicons-webfont.eot?#iefix&v=2.0.0') format('embedded-opentype'),url('../fonts/elusiveicons-webfont.woff?v=2.0.0') format('woff'),url('../fonts/elusiveicons-webfont.ttf?v=2.0.0') format('truetype'),url('../fonts/elusiveicons-webfont.svg?v=2.0.0#elusiveiconsregular') format('svg');font-weight:normal;font-style:normal}.el{display:inline-block;font:normal normal normal 14px/1 'Elusive-Icons';font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transform:translate(0, 0)}.el-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.el-2x{font-size:2em}.el-3x{font-size:3em}.el-4x{font-size:4em}.el-5x{font-size:5em}.el-fw{width:1.28571429em;text-align:center}.el-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.el-ul>li{position:relative}.el-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.el-li.el-lg{left:-1.85714286em}.el-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.el.pull-left{margin-right:.3em}.el.pull-right{margin-left:.3em}.el-spin{-webkit-animation:el-spin 2s infinite linear;animation:el-spin 2s infinite linear}.el-pulse{-webkit-animation:el-spin 1s infinite steps(8);animation:el-spin 1s infinite steps(8)}@-webkit-keyframes el-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes el-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.el-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.el-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.el-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.el-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.el-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .el-rotate-90,:root .el-rotate-180,:root .el-rotate-270,:root .el-flip-horizontal,:root .el-flip-vertical{filter:none}.el-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.el-stack-1x,.el-stack-2x{position:absolute;left:0;width:100%;text-align:center}.el-stack-1x{line-height:inherit}.el-stack-2x{font-size:2em}.el-inverse{color:#fff}.el-address-book-alt:before{content:"\f101"}.el-address-book:before{content:"\f102"}.el-adjust-alt:before{content:"\f103"}.el-adjust:before{content:"\f104"}.el-adult:before{content:"\f105"}.el-align-center:before{content:"\f106"}.el-align-justify:before{content:"\f107"}.el-align-left:before{content:"\f108"}.el-align-right:before{content:"\f109"}.el-arrow-down:before{content:"\f10a"}.el-arrow-left:before{content:"\f10b"}.el-arrow-right:before{content:"\f10c"}.el-arrow-up:before{content:"\f10d"}.el-asl:before{content:"\f10e"}.el-asterisk:before{content:"\f10f"}.el-backward:before{content:"\f110"}.el-ban-circle:before{content:"\f111"}.el-barcode:before{content:"\f112"}.el-behance:before{content:"\f113"}.el-bell:before{content:"\f114"}.el-blind:before{content:"\f115"}.el-blogger:before{content:"\f116"}.el-bold:before{content:"\f117"}.el-book:before{content:"\f118"}.el-bookmark-empty:before{content:"\f119"}.el-bookmark:before{content:"\f11a"}.el-braille:before{content:"\f11b"}.el-briefcase:before{content:"\f11c"}.el-broom:before{content:"\f11d"}.el-brush:before{content:"\f11e"}.el-bulb:before{content:"\f11f"}.el-bullhorn:before{content:"\f120"}.el-calendar-sign:before{content:"\f121"}.el-calendar:before{content:"\f122"}.el-camera:before{content:"\f123"}.el-car:before{content:"\f124"}.el-caret-down:before{content:"\f125"}.el-caret-left:before{content:"\f126"}.el-caret-right:before{content:"\f127"}.el-caret-up:before{content:"\f128"}.el-cc:before{content:"\f129"}.el-certificate:before{content:"\f12a"}.el-check-empty:before{content:"\f12b"}.el-check:before{content:"\f12c"}.el-chevron-down:before{content:"\f12d"}.el-chevron-left:before{content:"\f12e"}.el-chevron-right:before{content:"\f12f"}.el-chevron-up:before{content:"\f130"}.el-child:before{content:"\f131"}.el-circle-arrow-down:before{content:"\f132"}.el-circle-arrow-left:before{content:"\f133"}.el-circle-arrow-right:before{content:"\f134"}.el-circle-arrow-up:before{content:"\f135"}.el-cloud-alt:before{content:"\f136"}.el-cloud:before{content:"\f137"}.el-cog-alt:before{content:"\f138"}.el-cog:before{content:"\f139"}.el-cogs:before{content:"\f13a"}.el-comment-alt:before{content:"\f13b"}.el-comment:before{content:"\f13c"}.el-compass-alt:before{content:"\f13d"}.el-compass:before{content:"\f13e"}.el-credit-card:before{content:"\f13f"}.el-css:before{content:"\f140"}.el-dashboard:before{content:"\f141"}.el-delicious:before{content:"\f142"}.el-deviantart:before{content:"\f143"}.el-digg:before{content:"\f144"}.el-download-alt:before{content:"\f145"}.el-download:before{content:"\f146"}.el-dribbble:before{content:"\f147"}.el-edit:before{content:"\f148"}.el-eject:before{content:"\f149"}.el-envelope-alt:before{content:"\f14a"}.el-envelope:before{content:"\f14b"}.el-error-alt:before{content:"\f14c"}.el-error:before{content:"\f14d"}.el-eur:before{content:"\f14e"}.el-exclamation-sign:before{content:"\f14f"}.el-eye-close:before{content:"\f150"}.el-eye-open:before{content:"\f151"}.el-facebook:before{content:"\f152"}.el-facetime-video:before{content:"\f153"}.el-fast-backward:before{content:"\f154"}.el-fast-forward:before{content:"\f155"}.el-female:before{content:"\f156"}.el-file-alt:before{content:"\f157"}.el-file-edit-alt:before{content:"\f158"}.el-file-edit:before{content:"\f159"}.el-file-new-alt:before{content:"\f15a"}.el-file-new:before{content:"\f15b"}.el-file:before{content:"\f15c"}.el-film:before{content:"\f15d"}.el-filter:before{content:"\f15e"}.el-fire:before{content:"\f15f"}.el-flag-alt:before{content:"\f160"}.el-flag:before{content:"\f161"}.el-flickr:before{content:"\f162"}.el-folder-close:before{content:"\f163"}.el-folder-open:before{content:"\f164"}.el-folder-sign:before{content:"\f165"}.el-folder:before{content:"\f166"}.el-font:before{content:"\f167"}.el-fontsize:before{content:"\f168"}.el-fork:before{content:"\f169"}.el-forward-alt:before{content:"\f16a"}.el-forward:before{content:"\f16b"}.el-foursquare:before{content:"\f16c"}.el-friendfeed-rect:before{content:"\f16d"}.el-friendfeed:before{content:"\f16e"}.el-fullscreen:before{content:"\f16f"}.el-gbp:before{content:"\f170"}.el-gift:before{content:"\f171"}.el-github-text:before{content:"\f172"}.el-github:before{content:"\f173"}.el-glass:before{content:"\f174"}.el-glasses:before{content:"\f175"}.el-globe-alt:before{content:"\f176"}.el-globe:before{content:"\f177"}.el-googleplus:before{content:"\f178"}.el-graph-alt:before{content:"\f179"}.el-graph:before{content:"\f17a"}.el-group-alt:before{content:"\f17b"}.el-group:before{content:"\f17c"}.el-guidedog:before{content:"\f17d"}.el-hand-down:before{content:"\f17e"}.el-hand-left:before{content:"\f17f"}.el-hand-right:before{content:"\f180"}.el-hand-up:before{content:"\f181"}.el-hdd:before{content:"\f182"}.el-headphones:before{content:"\f183"}.el-hearing-impaired:before{content:"\f184"}.el-heart-alt:before{content:"\f185"}.el-heart-empty:before{content:"\f186"}.el-heart:before{content:"\f187"}.el-home-alt:before{content:"\f188"}.el-home:before{content:"\f189"}.el-hourglass:before{content:"\f18a"}.el-idea-alt:before{content:"\f18b"}.el-idea:before{content:"\f18c"}.el-inbox-alt:before{content:"\f18d"}.el-inbox-box:before{content:"\f18e"}.el-inbox:before{content:"\f18f"}.el-indent-left:before{content:"\f190"}.el-indent-right:before{content:"\f191"}.el-info-circle:before{content:"\f192"}.el-instagram:before{content:"\f193"}.el-iphone-home:before{content:"\f194"}.el-italic:before{content:"\f195"}.el-key:before{content:"\f196"}.el-laptop-alt:before{content:"\f197"}.el-laptop:before{content:"\f198"}.el-lastfm:before{content:"\f199"}.el-leaf:before{content:"\f19a"}.el-lines:before{content:"\f19b"}.el-link:before{content:"\f19c"}.el-linkedin:before{content:"\f19d"}.el-list-alt:before{content:"\f19e"}.el-list:before{content:"\f19f"}.el-livejournal:before{content:"\f1a0"}.el-lock-alt:before{content:"\f1a1"}.el-lock:before{content:"\f1a2"}.el-magic:before{content:"\f1a3"}.el-magnet:before{content:"\f1a4"}.el-male:before{content:"\f1a5"}.el-map-marker-alt:before{content:"\f1a6"}.el-map-marker:before{content:"\f1a7"}.el-mic-alt:before{content:"\f1a8"}.el-mic:before{content:"\f1a9"}.el-minus-sign:before{content:"\f1aa"}.el-minus:before{content:"\f1ab"}.el-move:before{content:"\f1ac"}.el-music:before{content:"\f1ad"}.el-myspace:before{content:"\f1ae"}.el-network:before{content:"\f1af"}.el-off:before{content:"\f1b0"}.el-ok-circle:before{content:"\f1b1"}.el-ok-sign:before{content:"\f1b2"}.el-ok:before{content:"\f1b3"}.el-opensource:before{content:"\f1b4"}.el-paper-clip-alt:before{content:"\f1b5"}.el-paper-clip:before{content:"\f1b6"}.el-path:before{content:"\f1b7"}.el-pause-alt:before{content:"\f1b8"}.el-pause:before{content:"\f1b9"}.el-pencil-alt:before{content:"\f1ba"}.el-pencil:before{content:"\f1bb"}.el-person:before{content:"\f1bc"}.el-phone-alt:before{content:"\f1bd"}.el-phone:before{content:"\f1be"}.el-photo-alt:before{content:"\f1bf"}.el-photo:before{content:"\f1c0"}.el-picasa:before{content:"\f1c1"}.el-picture:before{content:"\f1c2"}.el-pinterest:before{content:"\f1c3"}.el-plane:before{content:"\f1c4"}.el-play-alt:before{content:"\f1c5"}.el-play-circle:before{content:"\f1c6"}.el-play:before{content:"\f1c7"}.el-plurk-alt:before{content:"\f1c8"}.el-plurk:before{content:"\f1c9"}.el-plus-sign:before{content:"\f1ca"}.el-plus:before{content:"\f1cb"}.el-podcast:before{content:"\f1cc"}.el-print:before{content:"\f1cd"}.el-puzzle:before{content:"\f1ce"}.el-qrcode:before{content:"\f1cf"}.el-question-sign:before{content:"\f1d0"}.el-question:before{content:"\f1d1"}.el-quote-alt:before{content:"\f1d2"}.el-quote-right-alt:before{content:"\f1d3"}.el-quote-right:before{content:"\f1d4"}.el-quotes:before{content:"\f1d5"}.el-random:before{content:"\f1d6"}.el-record:before{content:"\f1d7"}.el-reddit:before{content:"\f1d8"}.el-redux:before{content:"\f1d9"}.el-refresh:before{content:"\f1da"}.el-remove-circle:before{content:"\f1db"}.el-remove-sign:before{content:"\f1dc"}.el-remove:before{content:"\f1dd"}.el-repeat-alt:before{content:"\f1de"}.el-repeat:before{content:"\f1df"}.el-resize-full:before{content:"\f1e0"}.el-resize-horizontal:before{content:"\f1e1"}.el-resize-small:before{content:"\f1e2"}.el-resize-vertical:before{content:"\f1e3"}.el-return-key:before{content:"\f1e4"}.el-retweet:before{content:"\f1e5"}.el-reverse-alt:before{content:"\f1e6"}.el-road:before{content:"\f1e7"}.el-rss:before{content:"\f1e8"}.el-scissors:before{content:"\f1e9"}.el-screen-alt:before{content:"\f1ea"}.el-screen:before{content:"\f1eb"}.el-screenshot:before{content:"\f1ec"}.el-search-alt:before{content:"\f1ed"}.el-search:before{content:"\f1ee"}.el-share-alt:before{content:"\f1ef"}.el-share:before{content:"\f1f0"}.el-shopping-cart-sign:before{content:"\f1f1"}.el-shopping-cart:before{content:"\f1f2"}.el-signal:before{content:"\f1f3"}.el-skype:before{content:"\f1f4"}.el-slideshare:before{content:"\f1f5"}.el-smiley-alt:before{content:"\f1f6"}.el-smiley:before{content:"\f1f7"}.el-soundcloud:before{content:"\f1f8"}.el-speaker:before{content:"\f1f9"}.el-spotify:before{content:"\f1fa"}.el-stackoverflow:before{content:"\f1fb"}.el-star-alt:before{content:"\f1fc"}.el-star-empty:before{content:"\f1fd"}.el-star:before{content:"\f1fe"}.el-step-backward:before{content:"\f1ff"}.el-step-forward:before{content:"\f200"}.el-stop-alt:before{content:"\f201"}.el-stop:before{content:"\f202"}.el-stumbleupon:before{content:"\f203"}.el-tag:before{content:"\f204"}.el-tags:before{content:"\f205"}.el-tasks:before{content:"\f206"}.el-text-height:before{content:"\f207"}.el-text-width:before{content:"\f208"}.el-th-large:before{content:"\f209"}.el-th-list:before{content:"\f20a"}.el-th:before{content:"\f20b"}.el-thumbs-down:before{content:"\f20c"}.el-thumbs-up:before{content:"\f20d"}.el-time-alt:before{content:"\f20e"}.el-time:before{content:"\f20f"}.el-tint:before{content:"\f210"}.el-torso:before{content:"\f211"}.el-trash-alt:before{content:"\f212"}.el-trash:before{content:"\f213"}.el-tumblr:before{content:"\f214"}.el-twitter:before{content:"\f215"}.el-universal-access:before{content:"\f216"}.el-unlock-alt:before{content:"\f217"}.el-unlock:before{content:"\f218"}.el-upload:before{content:"\f219"}.el-usd:before{content:"\f21a"}.el-user:before{content:"\f21b"}.el-viadeo:before{content:"\f21c"}.el-video-alt:before{content:"\f21d"}.el-video-chat:before{content:"\f21e"}.el-video:before{content:"\f21f"}.el-view-mode:before{content:"\f220"}.el-vimeo:before{content:"\f221"}.el-vkontakte:before{content:"\f222"}.el-volume-down:before{content:"\f223"}.el-volume-off:before{content:"\f224"}.el-volume-up:before{content:"\f225"}.el-w3c:before{content:"\f226"}.el-warning-sign:before{content:"\f227"}.el-website-alt:before{content:"\f228"}.el-website:before{content:"\f229"}.el-wheelchair:before{content:"\f22a"}.el-wordpress:before{content:"\f22b"}.el-wrench-alt:before{content:"\f22c"}.el-wrench:before{content:"\f22d"}.el-youtube:before{content:"\f22e"}.el-zoom-in:before{content:"\f22f"}.el-zoom-out:before{content:"\f230"}
diff --git a/Public/styles/elusive-icons/fonts/elusiveicons-webfont.eot b/Public/styles/elusive-icons/fonts/elusiveicons-webfont.eot
new file mode 100644
index 0000000..f42a001
Binary files /dev/null and b/Public/styles/elusive-icons/fonts/elusiveicons-webfont.eot differ
diff --git a/Public/styles/elusive-icons/fonts/elusiveicons-webfont.svg b/Public/styles/elusive-icons/fonts/elusiveicons-webfont.svg
new file mode 100644
index 0000000..1310e6c
--- /dev/null
+++ b/Public/styles/elusive-icons/fonts/elusiveicons-webfont.svg
@@ -0,0 +1,931 @@
+
+
+
+
+Created by FontForge 20120731 at Thu Feb 19 13:35:54 2015
+ By Dovy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public/styles/elusive-icons/fonts/elusiveicons-webfont.ttf b/Public/styles/elusive-icons/fonts/elusiveicons-webfont.ttf
new file mode 100644
index 0000000..b6fe85d
Binary files /dev/null and b/Public/styles/elusive-icons/fonts/elusiveicons-webfont.ttf differ
diff --git a/Public/styles/elusive-icons/fonts/elusiveicons-webfont.woff b/Public/styles/elusive-icons/fonts/elusiveicons-webfont.woff
new file mode 100644
index 0000000..1e0487d
Binary files /dev/null and b/Public/styles/elusive-icons/fonts/elusiveicons-webfont.woff differ
diff --git a/Public/styles/elusive-icons/less/animated.less b/Public/styles/elusive-icons/less/animated.less
new file mode 100644
index 0000000..c4efeb7
--- /dev/null
+++ b/Public/styles/elusive-icons/less/animated.less
@@ -0,0 +1,34 @@
+// Animated Icons
+// --------------------------
+
+.@{el-css-prefix}-spin {
+ -webkit-animation: el-spin 2s infinite linear;
+ animation: el-spin 2s infinite linear;
+}
+
+.@{el-css-prefix}-pulse {
+ -webkit-animation: el-spin 1s infinite steps(8);
+ animation: el-spin 1s infinite steps(8);
+}
+
+@-webkit-keyframes el-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+
+@keyframes el-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
diff --git a/Public/styles/elusive-icons/less/bordered-pulled.less b/Public/styles/elusive-icons/less/bordered-pulled.less
new file mode 100644
index 0000000..5896ddf
--- /dev/null
+++ b/Public/styles/elusive-icons/less/bordered-pulled.less
@@ -0,0 +1,16 @@
+// Bordered & Pulled
+// -------------------------
+
+.@{el-css-prefix}-border {
+ padding: .2em .25em .15em;
+ border: solid .08em @el-border-color;
+ border-radius: .1em;
+}
+
+.pull-right { float: right; }
+.pull-left { float: left; }
+
+.@{el-css-prefix} {
+ &.pull-left { margin-right: .3em; }
+ &.pull-right { margin-left: .3em; }
+}
diff --git a/Public/styles/elusive-icons/less/core.less b/Public/styles/elusive-icons/less/core.less
new file mode 100644
index 0000000..f29ac6e
--- /dev/null
+++ b/Public/styles/elusive-icons/less/core.less
@@ -0,0 +1,13 @@
+// Base Class Definition
+// -------------------------
+
+.@{el-css-prefix} {
+ display: inline-block;
+ font: normal normal normal @el-font-size-base/1 'Elusive-Icons'; // shortening font declaration
+ font-size: inherit; // can't have font-size inherit on line above, so need to override
+ text-rendering: auto; // optimizelegibility throws things off #1094
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ transform: translate(0, 0); // ensures no half-pixel rendering in firefox
+
+}
diff --git a/Public/styles/elusive-icons/less/elusive-icons.less b/Public/styles/elusive-icons/less/elusive-icons.less
new file mode 100644
index 0000000..f352477
--- /dev/null
+++ b/Public/styles/elusive-icons/less/elusive-icons.less
@@ -0,0 +1,17 @@
+/*!
+ * Elusive Icons 2.0.0 by @ReduxFramework - http://elusiveicons.com - @reduxframework
+ * License - http://elusiveicons.com/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */
+
+@import "variables.less";
+@import "mixins.less";
+@import "path.less";
+@import "core.less";
+@import "larger.less";
+@import "fixed-width.less";
+@import "list.less";
+@import "bordered-pulled.less";
+@import "animated.less";
+@import "rotated-flipped.less";
+@import "stacked.less";
+@import "icons.less";
diff --git a/Public/styles/elusive-icons/less/fixed-width.less b/Public/styles/elusive-icons/less/fixed-width.less
new file mode 100644
index 0000000..9f6a79a
--- /dev/null
+++ b/Public/styles/elusive-icons/less/fixed-width.less
@@ -0,0 +1,6 @@
+// Fixed Width Icons
+// -------------------------
+.@{el-css-prefix}-fw {
+ width: (18em / 14);
+ text-align: center;
+}
diff --git a/Public/styles/elusive-icons/less/icons.less b/Public/styles/elusive-icons/less/icons.less
new file mode 100644
index 0000000..908e3fa
--- /dev/null
+++ b/Public/styles/elusive-icons/less/icons.less
@@ -0,0 +1,307 @@
+/* Elusive Icons uses the Unicode Private Use Area (PUA) to ensure screen
+ readers do not read off random characters that represent icons */
+
+.@{el-css-prefix}-address-book-alt:before { content: @el-var-address-book-alt; }
+.@{el-css-prefix}-address-book:before { content: @el-var-address-book; }
+.@{el-css-prefix}-adjust-alt:before { content: @el-var-adjust-alt; }
+.@{el-css-prefix}-adjust:before { content: @el-var-adjust; }
+.@{el-css-prefix}-adult:before { content: @el-var-adult; }
+.@{el-css-prefix}-align-center:before { content: @el-var-align-center; }
+.@{el-css-prefix}-align-justify:before { content: @el-var-align-justify; }
+.@{el-css-prefix}-align-left:before { content: @el-var-align-left; }
+.@{el-css-prefix}-align-right:before { content: @el-var-align-right; }
+.@{el-css-prefix}-arrow-down:before { content: @el-var-arrow-down; }
+.@{el-css-prefix}-arrow-left:before { content: @el-var-arrow-left; }
+.@{el-css-prefix}-arrow-right:before { content: @el-var-arrow-right; }
+.@{el-css-prefix}-arrow-up:before { content: @el-var-arrow-up; }
+.@{el-css-prefix}-asl:before { content: @el-var-asl; }
+.@{el-css-prefix}-asterisk:before { content: @el-var-asterisk; }
+.@{el-css-prefix}-backward:before { content: @el-var-backward; }
+.@{el-css-prefix}-ban-circle:before { content: @el-var-ban-circle; }
+.@{el-css-prefix}-barcode:before { content: @el-var-barcode; }
+.@{el-css-prefix}-behance:before { content: @el-var-behance; }
+.@{el-css-prefix}-bell:before { content: @el-var-bell; }
+.@{el-css-prefix}-blind:before { content: @el-var-blind; }
+.@{el-css-prefix}-blogger:before { content: @el-var-blogger; }
+.@{el-css-prefix}-bold:before { content: @el-var-bold; }
+.@{el-css-prefix}-book:before { content: @el-var-book; }
+.@{el-css-prefix}-bookmark-empty:before { content: @el-var-bookmark-empty; }
+.@{el-css-prefix}-bookmark:before { content: @el-var-bookmark; }
+.@{el-css-prefix}-braille:before { content: @el-var-braille; }
+.@{el-css-prefix}-briefcase:before { content: @el-var-briefcase; }
+.@{el-css-prefix}-broom:before { content: @el-var-broom; }
+.@{el-css-prefix}-brush:before { content: @el-var-brush; }
+.@{el-css-prefix}-bulb:before { content: @el-var-bulb; }
+.@{el-css-prefix}-bullhorn:before { content: @el-var-bullhorn; }
+.@{el-css-prefix}-calendar-sign:before { content: @el-var-calendar-sign; }
+.@{el-css-prefix}-calendar:before { content: @el-var-calendar; }
+.@{el-css-prefix}-camera:before { content: @el-var-camera; }
+.@{el-css-prefix}-car:before { content: @el-var-car; }
+.@{el-css-prefix}-caret-down:before { content: @el-var-caret-down; }
+.@{el-css-prefix}-caret-left:before { content: @el-var-caret-left; }
+.@{el-css-prefix}-caret-right:before { content: @el-var-caret-right; }
+.@{el-css-prefix}-caret-up:before { content: @el-var-caret-up; }
+.@{el-css-prefix}-cc:before { content: @el-var-cc; }
+.@{el-css-prefix}-certificate:before { content: @el-var-certificate; }
+.@{el-css-prefix}-check-empty:before { content: @el-var-check-empty; }
+.@{el-css-prefix}-check:before { content: @el-var-check; }
+.@{el-css-prefix}-chevron-down:before { content: @el-var-chevron-down; }
+.@{el-css-prefix}-chevron-left:before { content: @el-var-chevron-left; }
+.@{el-css-prefix}-chevron-right:before { content: @el-var-chevron-right; }
+.@{el-css-prefix}-chevron-up:before { content: @el-var-chevron-up; }
+.@{el-css-prefix}-child:before { content: @el-var-child; }
+.@{el-css-prefix}-circle-arrow-down:before { content: @el-var-circle-arrow-down; }
+.@{el-css-prefix}-circle-arrow-left:before { content: @el-var-circle-arrow-left; }
+.@{el-css-prefix}-circle-arrow-right:before { content: @el-var-circle-arrow-right; }
+.@{el-css-prefix}-circle-arrow-up:before { content: @el-var-circle-arrow-up; }
+.@{el-css-prefix}-cloud-alt:before { content: @el-var-cloud-alt; }
+.@{el-css-prefix}-cloud:before { content: @el-var-cloud; }
+.@{el-css-prefix}-cog-alt:before { content: @el-var-cog-alt; }
+.@{el-css-prefix}-cog:before { content: @el-var-cog; }
+.@{el-css-prefix}-cogs:before { content: @el-var-cogs; }
+.@{el-css-prefix}-comment-alt:before { content: @el-var-comment-alt; }
+.@{el-css-prefix}-comment:before { content: @el-var-comment; }
+.@{el-css-prefix}-compass-alt:before { content: @el-var-compass-alt; }
+.@{el-css-prefix}-compass:before { content: @el-var-compass; }
+.@{el-css-prefix}-credit-card:before { content: @el-var-credit-card; }
+.@{el-css-prefix}-css:before { content: @el-var-css; }
+.@{el-css-prefix}-dashboard:before { content: @el-var-dashboard; }
+.@{el-css-prefix}-delicious:before { content: @el-var-delicious; }
+.@{el-css-prefix}-deviantart:before { content: @el-var-deviantart; }
+.@{el-css-prefix}-digg:before { content: @el-var-digg; }
+.@{el-css-prefix}-download-alt:before { content: @el-var-download-alt; }
+.@{el-css-prefix}-download:before { content: @el-var-download; }
+.@{el-css-prefix}-dribbble:before { content: @el-var-dribbble; }
+.@{el-css-prefix}-edit:before { content: @el-var-edit; }
+.@{el-css-prefix}-eject:before { content: @el-var-eject; }
+.@{el-css-prefix}-envelope-alt:before { content: @el-var-envelope-alt; }
+.@{el-css-prefix}-envelope:before { content: @el-var-envelope; }
+.@{el-css-prefix}-error-alt:before { content: @el-var-error-alt; }
+.@{el-css-prefix}-error:before { content: @el-var-error; }
+.@{el-css-prefix}-eur:before { content: @el-var-eur; }
+.@{el-css-prefix}-exclamation-sign:before { content: @el-var-exclamation-sign; }
+.@{el-css-prefix}-eye-close:before { content: @el-var-eye-close; }
+.@{el-css-prefix}-eye-open:before { content: @el-var-eye-open; }
+.@{el-css-prefix}-facebook:before { content: @el-var-facebook; }
+.@{el-css-prefix}-facetime-video:before { content: @el-var-facetime-video; }
+.@{el-css-prefix}-fast-backward:before { content: @el-var-fast-backward; }
+.@{el-css-prefix}-fast-forward:before { content: @el-var-fast-forward; }
+.@{el-css-prefix}-female:before { content: @el-var-female; }
+.@{el-css-prefix}-file-alt:before { content: @el-var-file-alt; }
+.@{el-css-prefix}-file-edit-alt:before { content: @el-var-file-edit-alt; }
+.@{el-css-prefix}-file-edit:before { content: @el-var-file-edit; }
+.@{el-css-prefix}-file-new-alt:before { content: @el-var-file-new-alt; }
+.@{el-css-prefix}-file-new:before { content: @el-var-file-new; }
+.@{el-css-prefix}-file:before { content: @el-var-file; }
+.@{el-css-prefix}-film:before { content: @el-var-film; }
+.@{el-css-prefix}-filter:before { content: @el-var-filter; }
+.@{el-css-prefix}-fire:before { content: @el-var-fire; }
+.@{el-css-prefix}-flag-alt:before { content: @el-var-flag-alt; }
+.@{el-css-prefix}-flag:before { content: @el-var-flag; }
+.@{el-css-prefix}-flickr:before { content: @el-var-flickr; }
+.@{el-css-prefix}-folder-close:before { content: @el-var-folder-close; }
+.@{el-css-prefix}-folder-open:before { content: @el-var-folder-open; }
+.@{el-css-prefix}-folder-sign:before { content: @el-var-folder-sign; }
+.@{el-css-prefix}-folder:before { content: @el-var-folder; }
+.@{el-css-prefix}-font:before { content: @el-var-font; }
+.@{el-css-prefix}-fontsize:before { content: @el-var-fontsize; }
+.@{el-css-prefix}-fork:before { content: @el-var-fork; }
+.@{el-css-prefix}-forward-alt:before { content: @el-var-forward-alt; }
+.@{el-css-prefix}-forward:before { content: @el-var-forward; }
+.@{el-css-prefix}-foursquare:before { content: @el-var-foursquare; }
+.@{el-css-prefix}-friendfeed-rect:before { content: @el-var-friendfeed-rect; }
+.@{el-css-prefix}-friendfeed:before { content: @el-var-friendfeed; }
+.@{el-css-prefix}-fullscreen:before { content: @el-var-fullscreen; }
+.@{el-css-prefix}-gbp:before { content: @el-var-gbp; }
+.@{el-css-prefix}-gift:before { content: @el-var-gift; }
+.@{el-css-prefix}-github-text:before { content: @el-var-github-text; }
+.@{el-css-prefix}-github:before { content: @el-var-github; }
+.@{el-css-prefix}-glass:before { content: @el-var-glass; }
+.@{el-css-prefix}-glasses:before { content: @el-var-glasses; }
+.@{el-css-prefix}-globe-alt:before { content: @el-var-globe-alt; }
+.@{el-css-prefix}-globe:before { content: @el-var-globe; }
+.@{el-css-prefix}-googleplus:before { content: @el-var-googleplus; }
+.@{el-css-prefix}-graph-alt:before { content: @el-var-graph-alt; }
+.@{el-css-prefix}-graph:before { content: @el-var-graph; }
+.@{el-css-prefix}-group-alt:before { content: @el-var-group-alt; }
+.@{el-css-prefix}-group:before { content: @el-var-group; }
+.@{el-css-prefix}-guidedog:before { content: @el-var-guidedog; }
+.@{el-css-prefix}-hand-down:before { content: @el-var-hand-down; }
+.@{el-css-prefix}-hand-left:before { content: @el-var-hand-left; }
+.@{el-css-prefix}-hand-right:before { content: @el-var-hand-right; }
+.@{el-css-prefix}-hand-up:before { content: @el-var-hand-up; }
+.@{el-css-prefix}-hdd:before { content: @el-var-hdd; }
+.@{el-css-prefix}-headphones:before { content: @el-var-headphones; }
+.@{el-css-prefix}-hearing-impaired:before { content: @el-var-hearing-impaired; }
+.@{el-css-prefix}-heart-alt:before { content: @el-var-heart-alt; }
+.@{el-css-prefix}-heart-empty:before { content: @el-var-heart-empty; }
+.@{el-css-prefix}-heart:before { content: @el-var-heart; }
+.@{el-css-prefix}-home-alt:before { content: @el-var-home-alt; }
+.@{el-css-prefix}-home:before { content: @el-var-home; }
+.@{el-css-prefix}-hourglass:before { content: @el-var-hourglass; }
+.@{el-css-prefix}-idea-alt:before { content: @el-var-idea-alt; }
+.@{el-css-prefix}-idea:before { content: @el-var-idea; }
+.@{el-css-prefix}-inbox-alt:before { content: @el-var-inbox-alt; }
+.@{el-css-prefix}-inbox-box:before { content: @el-var-inbox-box; }
+.@{el-css-prefix}-inbox:before { content: @el-var-inbox; }
+.@{el-css-prefix}-indent-left:before { content: @el-var-indent-left; }
+.@{el-css-prefix}-indent-right:before { content: @el-var-indent-right; }
+.@{el-css-prefix}-info-circle:before { content: @el-var-info-circle; }
+.@{el-css-prefix}-instagram:before { content: @el-var-instagram; }
+.@{el-css-prefix}-iphone-home:before { content: @el-var-iphone-home; }
+.@{el-css-prefix}-italic:before { content: @el-var-italic; }
+.@{el-css-prefix}-key:before { content: @el-var-key; }
+.@{el-css-prefix}-laptop-alt:before { content: @el-var-laptop-alt; }
+.@{el-css-prefix}-laptop:before { content: @el-var-laptop; }
+.@{el-css-prefix}-lastfm:before { content: @el-var-lastfm; }
+.@{el-css-prefix}-leaf:before { content: @el-var-leaf; }
+.@{el-css-prefix}-lines:before { content: @el-var-lines; }
+.@{el-css-prefix}-link:before { content: @el-var-link; }
+.@{el-css-prefix}-linkedin:before { content: @el-var-linkedin; }
+.@{el-css-prefix}-list-alt:before { content: @el-var-list-alt; }
+.@{el-css-prefix}-list:before { content: @el-var-list; }
+.@{el-css-prefix}-livejournal:before { content: @el-var-livejournal; }
+.@{el-css-prefix}-lock-alt:before { content: @el-var-lock-alt; }
+.@{el-css-prefix}-lock:before { content: @el-var-lock; }
+.@{el-css-prefix}-magic:before { content: @el-var-magic; }
+.@{el-css-prefix}-magnet:before { content: @el-var-magnet; }
+.@{el-css-prefix}-male:before { content: @el-var-male; }
+.@{el-css-prefix}-map-marker-alt:before { content: @el-var-map-marker-alt; }
+.@{el-css-prefix}-map-marker:before { content: @el-var-map-marker; }
+.@{el-css-prefix}-mic-alt:before { content: @el-var-mic-alt; }
+.@{el-css-prefix}-mic:before { content: @el-var-mic; }
+.@{el-css-prefix}-minus-sign:before { content: @el-var-minus-sign; }
+.@{el-css-prefix}-minus:before { content: @el-var-minus; }
+.@{el-css-prefix}-move:before { content: @el-var-move; }
+.@{el-css-prefix}-music:before { content: @el-var-music; }
+.@{el-css-prefix}-myspace:before { content: @el-var-myspace; }
+.@{el-css-prefix}-network:before { content: @el-var-network; }
+.@{el-css-prefix}-off:before { content: @el-var-off; }
+.@{el-css-prefix}-ok-circle:before { content: @el-var-ok-circle; }
+.@{el-css-prefix}-ok-sign:before { content: @el-var-ok-sign; }
+.@{el-css-prefix}-ok:before { content: @el-var-ok; }
+.@{el-css-prefix}-opensource:before { content: @el-var-opensource; }
+.@{el-css-prefix}-paper-clip-alt:before { content: @el-var-paper-clip-alt; }
+.@{el-css-prefix}-paper-clip:before { content: @el-var-paper-clip; }
+.@{el-css-prefix}-path:before { content: @el-var-path; }
+.@{el-css-prefix}-pause-alt:before { content: @el-var-pause-alt; }
+.@{el-css-prefix}-pause:before { content: @el-var-pause; }
+.@{el-css-prefix}-pencil-alt:before { content: @el-var-pencil-alt; }
+.@{el-css-prefix}-pencil:before { content: @el-var-pencil; }
+.@{el-css-prefix}-person:before { content: @el-var-person; }
+.@{el-css-prefix}-phone-alt:before { content: @el-var-phone-alt; }
+.@{el-css-prefix}-phone:before { content: @el-var-phone; }
+.@{el-css-prefix}-photo-alt:before { content: @el-var-photo-alt; }
+.@{el-css-prefix}-photo:before { content: @el-var-photo; }
+.@{el-css-prefix}-picasa:before { content: @el-var-picasa; }
+.@{el-css-prefix}-picture:before { content: @el-var-picture; }
+.@{el-css-prefix}-pinterest:before { content: @el-var-pinterest; }
+.@{el-css-prefix}-plane:before { content: @el-var-plane; }
+.@{el-css-prefix}-play-alt:before { content: @el-var-play-alt; }
+.@{el-css-prefix}-play-circle:before { content: @el-var-play-circle; }
+.@{el-css-prefix}-play:before { content: @el-var-play; }
+.@{el-css-prefix}-plurk-alt:before { content: @el-var-plurk-alt; }
+.@{el-css-prefix}-plurk:before { content: @el-var-plurk; }
+.@{el-css-prefix}-plus-sign:before { content: @el-var-plus-sign; }
+.@{el-css-prefix}-plus:before { content: @el-var-plus; }
+.@{el-css-prefix}-podcast:before { content: @el-var-podcast; }
+.@{el-css-prefix}-print:before { content: @el-var-print; }
+.@{el-css-prefix}-puzzle:before { content: @el-var-puzzle; }
+.@{el-css-prefix}-qrcode:before { content: @el-var-qrcode; }
+.@{el-css-prefix}-question-sign:before { content: @el-var-question-sign; }
+.@{el-css-prefix}-question:before { content: @el-var-question; }
+.@{el-css-prefix}-quote-alt:before { content: @el-var-quote-alt; }
+.@{el-css-prefix}-quote-right-alt:before { content: @el-var-quote-right-alt; }
+.@{el-css-prefix}-quote-right:before { content: @el-var-quote-right; }
+.@{el-css-prefix}-quotes:before { content: @el-var-quotes; }
+.@{el-css-prefix}-random:before { content: @el-var-random; }
+.@{el-css-prefix}-record:before { content: @el-var-record; }
+.@{el-css-prefix}-reddit:before { content: @el-var-reddit; }
+.@{el-css-prefix}-redux:before { content: @el-var-redux; }
+.@{el-css-prefix}-refresh:before { content: @el-var-refresh; }
+.@{el-css-prefix}-remove-circle:before { content: @el-var-remove-circle; }
+.@{el-css-prefix}-remove-sign:before { content: @el-var-remove-sign; }
+.@{el-css-prefix}-remove:before { content: @el-var-remove; }
+.@{el-css-prefix}-repeat-alt:before { content: @el-var-repeat-alt; }
+.@{el-css-prefix}-repeat:before { content: @el-var-repeat; }
+.@{el-css-prefix}-resize-full:before { content: @el-var-resize-full; }
+.@{el-css-prefix}-resize-horizontal:before { content: @el-var-resize-horizontal; }
+.@{el-css-prefix}-resize-small:before { content: @el-var-resize-small; }
+.@{el-css-prefix}-resize-vertical:before { content: @el-var-resize-vertical; }
+.@{el-css-prefix}-return-key:before { content: @el-var-return-key; }
+.@{el-css-prefix}-retweet:before { content: @el-var-retweet; }
+.@{el-css-prefix}-reverse-alt:before { content: @el-var-reverse-alt; }
+.@{el-css-prefix}-road:before { content: @el-var-road; }
+.@{el-css-prefix}-rss:before { content: @el-var-rss; }
+.@{el-css-prefix}-scissors:before { content: @el-var-scissors; }
+.@{el-css-prefix}-screen-alt:before { content: @el-var-screen-alt; }
+.@{el-css-prefix}-screen:before { content: @el-var-screen; }
+.@{el-css-prefix}-screenshot:before { content: @el-var-screenshot; }
+.@{el-css-prefix}-search-alt:before { content: @el-var-search-alt; }
+.@{el-css-prefix}-search:before { content: @el-var-search; }
+.@{el-css-prefix}-share-alt:before { content: @el-var-share-alt; }
+.@{el-css-prefix}-share:before { content: @el-var-share; }
+.@{el-css-prefix}-shopping-cart-sign:before { content: @el-var-shopping-cart-sign; }
+.@{el-css-prefix}-shopping-cart:before { content: @el-var-shopping-cart; }
+.@{el-css-prefix}-signal:before { content: @el-var-signal; }
+.@{el-css-prefix}-skype:before { content: @el-var-skype; }
+.@{el-css-prefix}-slideshare:before { content: @el-var-slideshare; }
+.@{el-css-prefix}-smiley-alt:before { content: @el-var-smiley-alt; }
+.@{el-css-prefix}-smiley:before { content: @el-var-smiley; }
+.@{el-css-prefix}-soundcloud:before { content: @el-var-soundcloud; }
+.@{el-css-prefix}-speaker:before { content: @el-var-speaker; }
+.@{el-css-prefix}-spotify:before { content: @el-var-spotify; }
+.@{el-css-prefix}-stackoverflow:before { content: @el-var-stackoverflow; }
+.@{el-css-prefix}-star-alt:before { content: @el-var-star-alt; }
+.@{el-css-prefix}-star-empty:before { content: @el-var-star-empty; }
+.@{el-css-prefix}-star:before { content: @el-var-star; }
+.@{el-css-prefix}-step-backward:before { content: @el-var-step-backward; }
+.@{el-css-prefix}-step-forward:before { content: @el-var-step-forward; }
+.@{el-css-prefix}-stop-alt:before { content: @el-var-stop-alt; }
+.@{el-css-prefix}-stop:before { content: @el-var-stop; }
+.@{el-css-prefix}-stumbleupon:before { content: @el-var-stumbleupon; }
+.@{el-css-prefix}-tag:before { content: @el-var-tag; }
+.@{el-css-prefix}-tags:before { content: @el-var-tags; }
+.@{el-css-prefix}-tasks:before { content: @el-var-tasks; }
+.@{el-css-prefix}-text-height:before { content: @el-var-text-height; }
+.@{el-css-prefix}-text-width:before { content: @el-var-text-width; }
+.@{el-css-prefix}-th-large:before { content: @el-var-th-large; }
+.@{el-css-prefix}-th-list:before { content: @el-var-th-list; }
+.@{el-css-prefix}-th:before { content: @el-var-th; }
+.@{el-css-prefix}-thumbs-down:before { content: @el-var-thumbs-down; }
+.@{el-css-prefix}-thumbs-up:before { content: @el-var-thumbs-up; }
+.@{el-css-prefix}-time-alt:before { content: @el-var-time-alt; }
+.@{el-css-prefix}-time:before { content: @el-var-time; }
+.@{el-css-prefix}-tint:before { content: @el-var-tint; }
+.@{el-css-prefix}-torso:before { content: @el-var-torso; }
+.@{el-css-prefix}-trash-alt:before { content: @el-var-trash-alt; }
+.@{el-css-prefix}-trash:before { content: @el-var-trash; }
+.@{el-css-prefix}-tumblr:before { content: @el-var-tumblr; }
+.@{el-css-prefix}-twitter:before { content: @el-var-twitter; }
+.@{el-css-prefix}-universal-access:before { content: @el-var-universal-access; }
+.@{el-css-prefix}-unlock-alt:before { content: @el-var-unlock-alt; }
+.@{el-css-prefix}-unlock:before { content: @el-var-unlock; }
+.@{el-css-prefix}-upload:before { content: @el-var-upload; }
+.@{el-css-prefix}-usd:before { content: @el-var-usd; }
+.@{el-css-prefix}-user:before { content: @el-var-user; }
+.@{el-css-prefix}-viadeo:before { content: @el-var-viadeo; }
+.@{el-css-prefix}-video-alt:before { content: @el-var-video-alt; }
+.@{el-css-prefix}-video-chat:before { content: @el-var-video-chat; }
+.@{el-css-prefix}-video:before { content: @el-var-video; }
+.@{el-css-prefix}-view-mode:before { content: @el-var-view-mode; }
+.@{el-css-prefix}-vimeo:before { content: @el-var-vimeo; }
+.@{el-css-prefix}-vkontakte:before { content: @el-var-vkontakte; }
+.@{el-css-prefix}-volume-down:before { content: @el-var-volume-down; }
+.@{el-css-prefix}-volume-off:before { content: @el-var-volume-off; }
+.@{el-css-prefix}-volume-up:before { content: @el-var-volume-up; }
+.@{el-css-prefix}-w3c:before { content: @el-var-w3c; }
+.@{el-css-prefix}-warning-sign:before { content: @el-var-warning-sign; }
+.@{el-css-prefix}-website-alt:before { content: @el-var-website-alt; }
+.@{el-css-prefix}-website:before { content: @el-var-website; }
+.@{el-css-prefix}-wheelchair:before { content: @el-var-wheelchair; }
+.@{el-css-prefix}-wordpress:before { content: @el-var-wordpress; }
+.@{el-css-prefix}-wrench-alt:before { content: @el-var-wrench-alt; }
+.@{el-css-prefix}-wrench:before { content: @el-var-wrench; }
+.@{el-css-prefix}-youtube:before { content: @el-var-youtube; }
+.@{el-css-prefix}-zoom-in:before { content: @el-var-zoom-in; }
+.@{el-css-prefix}-zoom-out:before { content: @el-var-zoom-out; }
diff --git a/Public/styles/elusive-icons/less/larger.less b/Public/styles/elusive-icons/less/larger.less
new file mode 100644
index 0000000..b5d8798
--- /dev/null
+++ b/Public/styles/elusive-icons/less/larger.less
@@ -0,0 +1,13 @@
+// Icon Sizes
+// -------------------------
+
+/* makes the font 33% larger relative to the icon container */
+.@{el-css-prefix}-lg {
+ font-size: (4em / 3);
+ line-height: (3em / 4);
+ vertical-align: -15%;
+}
+.@{el-css-prefix}-2x { font-size: 2em; }
+.@{el-css-prefix}-3x { font-size: 3em; }
+.@{el-css-prefix}-4x { font-size: 4em; }
+.@{el-css-prefix}-5x { font-size: 5em; }
diff --git a/Public/styles/elusive-icons/less/list.less b/Public/styles/elusive-icons/less/list.less
new file mode 100644
index 0000000..24d1136
--- /dev/null
+++ b/Public/styles/elusive-icons/less/list.less
@@ -0,0 +1,19 @@
+// List Icons
+// -------------------------
+
+.@{el-css-prefix}-ul {
+ padding-left: 0;
+ margin-left: @el-li-width;
+ list-style-type: none;
+ > li { position: relative; }
+}
+.@{el-css-prefix}-li {
+ position: absolute;
+ left: -@el-li-width;
+ width: @el-li-width;
+ top: (2em / 14);
+ text-align: center;
+ &.@{el-css-prefix}-lg {
+ left: (-@el-li-width + (4em / 14));
+ }
+}
diff --git a/Public/styles/elusive-icons/less/mixins.less b/Public/styles/elusive-icons/less/mixins.less
new file mode 100644
index 0000000..4f1ed5b
--- /dev/null
+++ b/Public/styles/elusive-icons/less/mixins.less
@@ -0,0 +1,27 @@
+// Mixins
+// --------------------------
+
+.el-icon() {
+ display: inline-block;
+ font: normal normal normal @el-font-size-base/1 'Elusive-Icons'; // shortening font declaration
+ font-size: inherit; // can't have font-size inherit on line above, so need to override
+ text-rendering: auto; // optimizelegibility throws things off #1094
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ transform: translate(0, 0); // ensures no half-pixel rendering in firefox
+
+}
+
+.el-icon-rotate(@degrees, @rotation) {
+ filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation);
+ -webkit-transform: rotate(@degrees);
+ -ms-transform: rotate(@degrees);
+ transform: rotate(@degrees);
+}
+
+.el-icon-flip(@horiz, @vert, @rotation) {
+ filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation, mirror=1);
+ -webkit-transform: scale(@horiz, @vert);
+ -ms-transform: scale(@horiz, @vert);
+ transform: scale(@horiz, @vert);
+}
diff --git a/Public/styles/elusive-icons/less/path.less b/Public/styles/elusive-icons/less/path.less
new file mode 100644
index 0000000..96fd8a9
--- /dev/null
+++ b/Public/styles/elusive-icons/less/path.less
@@ -0,0 +1,14 @@
+/* FONT PATH
+ * -------------------------- */
+
+@font-face {
+ font-family: 'Elusive-Icons';
+ src: url('@{el-font-path}/elusiveicons-webfont.eot?v=@{el-version}');
+ src: url('@{el-font-path}/elusiveicons-webfont.eot?#iefix&v=@{el-version}') format('embedded-opentype'),
+ //url('@{el-font-path}/elusiveicons-webfont.woff2?v=@{el-version}') format('woff2'),
+ url('@{el-font-path}/elusiveicons-webfont.woff?v=@{el-version}') format('woff'),
+ url('@{el-font-path}/elusiveicons-webfont.ttf?v=@{el-version}') format('truetype'),
+ url('@{el-font-path}/elusiveicons-webfont.svg?v=@{el-version}#elusiveiconsregular') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
diff --git a/Public/styles/elusive-icons/less/rotated-flipped.less b/Public/styles/elusive-icons/less/rotated-flipped.less
new file mode 100644
index 0000000..26e8259
--- /dev/null
+++ b/Public/styles/elusive-icons/less/rotated-flipped.less
@@ -0,0 +1,20 @@
+// Rotated & Flipped Icons
+// -------------------------
+
+.@{el-css-prefix}-rotate-90 { .el-icon-rotate(90deg, 1); }
+.@{el-css-prefix}-rotate-180 { .el-icon-rotate(180deg, 2); }
+.@{el-css-prefix}-rotate-270 { .el-icon-rotate(270deg, 3); }
+
+.@{el-css-prefix}-flip-horizontal { .el-icon-flip(-1, 1, 0); }
+.@{el-css-prefix}-flip-vertical { .el-icon-flip(1, -1, 2); }
+
+// Hook for IE8-9
+// -------------------------
+
+:root .@{el-css-prefix}-rotate-90,
+:root .@{el-css-prefix}-rotate-180,
+:root .@{el-css-prefix}-rotate-270,
+:root .@{el-css-prefix}-flip-horizontal,
+:root .@{el-css-prefix}-flip-vertical {
+ filter: none;
+}
diff --git a/Public/styles/elusive-icons/less/stacked.less b/Public/styles/elusive-icons/less/stacked.less
new file mode 100644
index 0000000..d244d6c
--- /dev/null
+++ b/Public/styles/elusive-icons/less/stacked.less
@@ -0,0 +1,20 @@
+// Stacked Icons
+// -------------------------
+
+.@{el-css-prefix}-stack {
+ position: relative;
+ display: inline-block;
+ width: 2em;
+ height: 2em;
+ line-height: 2em;
+ vertical-align: middle;
+}
+.@{el-css-prefix}-stack-1x, .@{el-css-prefix}-stack-2x {
+ position: absolute;
+ left: 0;
+ width: 100%;
+ text-align: center;
+}
+.@{el-css-prefix}-stack-1x { line-height: inherit; }
+.@{el-css-prefix}-stack-2x { font-size: 2em; }
+.@{el-css-prefix}-inverse { color: @el-inverse; }
diff --git a/Public/styles/elusive-icons/less/variables.less b/Public/styles/elusive-icons/less/variables.less
new file mode 100644
index 0000000..fab56d3
--- /dev/null
+++ b/Public/styles/elusive-icons/less/variables.less
@@ -0,0 +1,317 @@
+// Variables
+// --------------------------
+
+@el-font-path: "../fonts";
+@el-font-size-base: 14px;
+//@el-font-path: "//netdna.bootstrapcdn.com/elusive-icons/2.0.0/fonts"; // for referencing Bootstrap CDN font files directly
+@el-css-prefix: el;
+@el-version: "2.0.0";
+@el-border-color: #eee;
+@el-inverse: #fff;
+@el-li-width: (30em / 14);
+
+@el-var-address-book: "\f102";
+@el-var-address-book-alt: "\f101";
+@el-var-adjust: "\f104";
+@el-var-adjust-alt: "\f103";
+@el-var-adult: "\f105";
+@el-var-align-center: "\f106";
+@el-var-align-justify: "\f107";
+@el-var-align-left: "\f108";
+@el-var-align-right: "\f109";
+@el-var-arrow-down: "\f10a";
+@el-var-arrow-left: "\f10b";
+@el-var-arrow-right: "\f10c";
+@el-var-arrow-up: "\f10d";
+@el-var-asl: "\f10e";
+@el-var-asterisk: "\f10f";
+@el-var-backward: "\f110";
+@el-var-ban-circle: "\f111";
+@el-var-barcode: "\f112";
+@el-var-behance: "\f113";
+@el-var-bell: "\f114";
+@el-var-blind: "\f115";
+@el-var-blogger: "\f116";
+@el-var-bold: "\f117";
+@el-var-book: "\f118";
+@el-var-bookmark: "\f11a";
+@el-var-bookmark-empty: "\f119";
+@el-var-braille: "\f11b";
+@el-var-briefcase: "\f11c";
+@el-var-broom: "\f11d";
+@el-var-brush: "\f11e";
+@el-var-bulb: "\f11f";
+@el-var-bullhorn: "\f120";
+@el-var-calendar: "\f122";
+@el-var-calendar-sign: "\f121";
+@el-var-camera: "\f123";
+@el-var-car: "\f124";
+@el-var-caret-down: "\f125";
+@el-var-caret-left: "\f126";
+@el-var-caret-right: "\f127";
+@el-var-caret-up: "\f128";
+@el-var-cc: "\f129";
+@el-var-certificate: "\f12a";
+@el-var-check: "\f12c";
+@el-var-check-empty: "\f12b";
+@el-var-chevron-down: "\f12d";
+@el-var-chevron-left: "\f12e";
+@el-var-chevron-right: "\f12f";
+@el-var-chevron-up: "\f130";
+@el-var-child: "\f131";
+@el-var-circle-arrow-down: "\f132";
+@el-var-circle-arrow-left: "\f133";
+@el-var-circle-arrow-right: "\f134";
+@el-var-circle-arrow-up: "\f135";
+@el-var-cloud: "\f137";
+@el-var-cloud-alt: "\f136";
+@el-var-cog: "\f139";
+@el-var-cog-alt: "\f138";
+@el-var-cogs: "\f13a";
+@el-var-comment: "\f13c";
+@el-var-comment-alt: "\f13b";
+@el-var-compass: "\f13e";
+@el-var-compass-alt: "\f13d";
+@el-var-credit-card: "\f13f";
+@el-var-css: "\f140";
+@el-var-dashboard: "\f141";
+@el-var-delicious: "\f142";
+@el-var-deviantart: "\f143";
+@el-var-digg: "\f144";
+@el-var-download: "\f146";
+@el-var-download-alt: "\f145";
+@el-var-dribbble: "\f147";
+@el-var-edit: "\f148";
+@el-var-eject: "\f149";
+@el-var-envelope: "\f14b";
+@el-var-envelope-alt: "\f14a";
+@el-var-error: "\f14d";
+@el-var-error-alt: "\f14c";
+@el-var-eur: "\f14e";
+@el-var-exclamation-sign: "\f14f";
+@el-var-eye-close: "\f150";
+@el-var-eye-open: "\f151";
+@el-var-facebook: "\f152";
+@el-var-facetime-video: "\f153";
+@el-var-fast-backward: "\f154";
+@el-var-fast-forward: "\f155";
+@el-var-female: "\f156";
+@el-var-file: "\f15c";
+@el-var-file-alt: "\f157";
+@el-var-file-edit: "\f159";
+@el-var-file-edit-alt: "\f158";
+@el-var-file-new: "\f15b";
+@el-var-file-new-alt: "\f15a";
+@el-var-film: "\f15d";
+@el-var-filter: "\f15e";
+@el-var-fire: "\f15f";
+@el-var-flag: "\f161";
+@el-var-flag-alt: "\f160";
+@el-var-flickr: "\f162";
+@el-var-folder: "\f166";
+@el-var-folder-close: "\f163";
+@el-var-folder-open: "\f164";
+@el-var-folder-sign: "\f165";
+@el-var-font: "\f167";
+@el-var-fontsize: "\f168";
+@el-var-fork: "\f169";
+@el-var-forward: "\f16b";
+@el-var-forward-alt: "\f16a";
+@el-var-foursquare: "\f16c";
+@el-var-friendfeed: "\f16e";
+@el-var-friendfeed-rect: "\f16d";
+@el-var-fullscreen: "\f16f";
+@el-var-gbp: "\f170";
+@el-var-gift: "\f171";
+@el-var-github: "\f173";
+@el-var-github-text: "\f172";
+@el-var-glass: "\f174";
+@el-var-glasses: "\f175";
+@el-var-globe: "\f177";
+@el-var-globe-alt: "\f176";
+@el-var-googleplus: "\f178";
+@el-var-graph: "\f17a";
+@el-var-graph-alt: "\f179";
+@el-var-group: "\f17c";
+@el-var-group-alt: "\f17b";
+@el-var-guidedog: "\f17d";
+@el-var-hand-down: "\f17e";
+@el-var-hand-left: "\f17f";
+@el-var-hand-right: "\f180";
+@el-var-hand-up: "\f181";
+@el-var-hdd: "\f182";
+@el-var-headphones: "\f183";
+@el-var-hearing-impaired: "\f184";
+@el-var-heart: "\f187";
+@el-var-heart-alt: "\f185";
+@el-var-heart-empty: "\f186";
+@el-var-home: "\f189";
+@el-var-home-alt: "\f188";
+@el-var-hourglass: "\f18a";
+@el-var-idea: "\f18c";
+@el-var-idea-alt: "\f18b";
+@el-var-inbox: "\f18f";
+@el-var-inbox-alt: "\f18d";
+@el-var-inbox-box: "\f18e";
+@el-var-indent-left: "\f190";
+@el-var-indent-right: "\f191";
+@el-var-info-circle: "\f192";
+@el-var-instagram: "\f193";
+@el-var-iphone-home: "\f194";
+@el-var-italic: "\f195";
+@el-var-key: "\f196";
+@el-var-laptop: "\f198";
+@el-var-laptop-alt: "\f197";
+@el-var-lastfm: "\f199";
+@el-var-leaf: "\f19a";
+@el-var-lines: "\f19b";
+@el-var-link: "\f19c";
+@el-var-linkedin: "\f19d";
+@el-var-list: "\f19f";
+@el-var-list-alt: "\f19e";
+@el-var-livejournal: "\f1a0";
+@el-var-lock: "\f1a2";
+@el-var-lock-alt: "\f1a1";
+@el-var-magic: "\f1a3";
+@el-var-magnet: "\f1a4";
+@el-var-male: "\f1a5";
+@el-var-map-marker: "\f1a7";
+@el-var-map-marker-alt: "\f1a6";
+@el-var-mic: "\f1a9";
+@el-var-mic-alt: "\f1a8";
+@el-var-minus: "\f1ab";
+@el-var-minus-sign: "\f1aa";
+@el-var-move: "\f1ac";
+@el-var-music: "\f1ad";
+@el-var-myspace: "\f1ae";
+@el-var-network: "\f1af";
+@el-var-off: "\f1b0";
+@el-var-ok: "\f1b3";
+@el-var-ok-circle: "\f1b1";
+@el-var-ok-sign: "\f1b2";
+@el-var-opensource: "\f1b4";
+@el-var-paper-clip: "\f1b6";
+@el-var-paper-clip-alt: "\f1b5";
+@el-var-path: "\f1b7";
+@el-var-pause: "\f1b9";
+@el-var-pause-alt: "\f1b8";
+@el-var-pencil: "\f1bb";
+@el-var-pencil-alt: "\f1ba";
+@el-var-person: "\f1bc";
+@el-var-phone: "\f1be";
+@el-var-phone-alt: "\f1bd";
+@el-var-photo: "\f1c0";
+@el-var-photo-alt: "\f1bf";
+@el-var-picasa: "\f1c1";
+@el-var-picture: "\f1c2";
+@el-var-pinterest: "\f1c3";
+@el-var-plane: "\f1c4";
+@el-var-play: "\f1c7";
+@el-var-play-alt: "\f1c5";
+@el-var-play-circle: "\f1c6";
+@el-var-plurk: "\f1c9";
+@el-var-plurk-alt: "\f1c8";
+@el-var-plus: "\f1cb";
+@el-var-plus-sign: "\f1ca";
+@el-var-podcast: "\f1cc";
+@el-var-print: "\f1cd";
+@el-var-puzzle: "\f1ce";
+@el-var-qrcode: "\f1cf";
+@el-var-question: "\f1d1";
+@el-var-question-sign: "\f1d0";
+@el-var-quote-alt: "\f1d2";
+@el-var-quote-right: "\f1d4";
+@el-var-quote-right-alt: "\f1d3";
+@el-var-quotes: "\f1d5";
+@el-var-random: "\f1d6";
+@el-var-record: "\f1d7";
+@el-var-reddit: "\f1d8";
+@el-var-redux: "\f1d9";
+@el-var-refresh: "\f1da";
+@el-var-remove: "\f1dd";
+@el-var-remove-circle: "\f1db";
+@el-var-remove-sign: "\f1dc";
+@el-var-repeat: "\f1df";
+@el-var-repeat-alt: "\f1de";
+@el-var-resize-full: "\f1e0";
+@el-var-resize-horizontal: "\f1e1";
+@el-var-resize-small: "\f1e2";
+@el-var-resize-vertical: "\f1e3";
+@el-var-return-key: "\f1e4";
+@el-var-retweet: "\f1e5";
+@el-var-reverse-alt: "\f1e6";
+@el-var-road: "\f1e7";
+@el-var-rss: "\f1e8";
+@el-var-scissors: "\f1e9";
+@el-var-screen: "\f1eb";
+@el-var-screen-alt: "\f1ea";
+@el-var-screenshot: "\f1ec";
+@el-var-search: "\f1ee";
+@el-var-search-alt: "\f1ed";
+@el-var-share: "\f1f0";
+@el-var-share-alt: "\f1ef";
+@el-var-shopping-cart: "\f1f2";
+@el-var-shopping-cart-sign: "\f1f1";
+@el-var-signal: "\f1f3";
+@el-var-skype: "\f1f4";
+@el-var-slideshare: "\f1f5";
+@el-var-smiley: "\f1f7";
+@el-var-smiley-alt: "\f1f6";
+@el-var-soundcloud: "\f1f8";
+@el-var-speaker: "\f1f9";
+@el-var-spotify: "\f1fa";
+@el-var-stackoverflow: "\f1fb";
+@el-var-star: "\f1fe";
+@el-var-star-alt: "\f1fc";
+@el-var-star-empty: "\f1fd";
+@el-var-step-backward: "\f1ff";
+@el-var-step-forward: "\f200";
+@el-var-stop: "\f202";
+@el-var-stop-alt: "\f201";
+@el-var-stumbleupon: "\f203";
+@el-var-tag: "\f204";
+@el-var-tags: "\f205";
+@el-var-tasks: "\f206";
+@el-var-text-height: "\f207";
+@el-var-text-width: "\f208";
+@el-var-th: "\f20b";
+@el-var-th-large: "\f209";
+@el-var-th-list: "\f20a";
+@el-var-thumbs-down: "\f20c";
+@el-var-thumbs-up: "\f20d";
+@el-var-time: "\f20f";
+@el-var-time-alt: "\f20e";
+@el-var-tint: "\f210";
+@el-var-torso: "\f211";
+@el-var-trash: "\f213";
+@el-var-trash-alt: "\f212";
+@el-var-tumblr: "\f214";
+@el-var-twitter: "\f215";
+@el-var-universal-access: "\f216";
+@el-var-unlock: "\f218";
+@el-var-unlock-alt: "\f217";
+@el-var-upload: "\f219";
+@el-var-usd: "\f21a";
+@el-var-user: "\f21b";
+@el-var-viadeo: "\f21c";
+@el-var-video: "\f21f";
+@el-var-video-alt: "\f21d";
+@el-var-video-chat: "\f21e";
+@el-var-view-mode: "\f220";
+@el-var-vimeo: "\f221";
+@el-var-vkontakte: "\f222";
+@el-var-volume-down: "\f223";
+@el-var-volume-off: "\f224";
+@el-var-volume-up: "\f225";
+@el-var-w3c: "\f226";
+@el-var-warning-sign: "\f227";
+@el-var-website: "\f229";
+@el-var-website-alt: "\f228";
+@el-var-wheelchair: "\f22a";
+@el-var-wordpress: "\f22b";
+@el-var-wrench: "\f22d";
+@el-var-wrench-alt: "\f22c";
+@el-var-youtube: "\f22e";
+@el-var-zoom-in: "\f22f";
+@el-var-zoom-out: "\f230";
+
diff --git a/Public/styles/elusive-icons/scss/_animated.scss b/Public/styles/elusive-icons/scss/_animated.scss
new file mode 100644
index 0000000..7661bb0
--- /dev/null
+++ b/Public/styles/elusive-icons/scss/_animated.scss
@@ -0,0 +1,34 @@
+// Spinning Icons
+// --------------------------
+
+.#{$el-css-prefix}-spin {
+ -webkit-animation: el-spin 2s infinite linear;
+ animation: el-spin 2s infinite linear;
+}
+
+.#{$el-css-prefix}-pulse {
+ -webkit-animation: el-spin 1s infinite steps(8);
+ animation: el-spin 1s infinite steps(8);
+}
+
+@-webkit-keyframes el-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+
+@keyframes el-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
diff --git a/Public/styles/elusive-icons/scss/_bordered-pulled.scss b/Public/styles/elusive-icons/scss/_bordered-pulled.scss
new file mode 100644
index 0000000..c5a0858
--- /dev/null
+++ b/Public/styles/elusive-icons/scss/_bordered-pulled.scss
@@ -0,0 +1,16 @@
+// Bordered & Pulled
+// -------------------------
+
+.#{$el-css-prefix}-border {
+ padding: .2em .25em .15em;
+ border: solid .08em $el-border-color;
+ border-radius: .1em;
+}
+
+.pull-right { float: right; }
+.pull-left { float: left; }
+
+.#{$el-css-prefix} {
+ &.pull-left { margin-right: .3em; }
+ &.pull-right { margin-left: .3em; }
+}
diff --git a/Public/styles/elusive-icons/scss/_core.scss b/Public/styles/elusive-icons/scss/_core.scss
new file mode 100644
index 0000000..e987377
--- /dev/null
+++ b/Public/styles/elusive-icons/scss/_core.scss
@@ -0,0 +1,13 @@
+// Base Class Definition
+// -------------------------
+
+.#{$el-css-prefix} {
+ display: inline-block;
+ font: normal normal normal #{$el-font-size-base}/1 'Elusive-Icons'; // shortening font declaration
+ font-size: inherit; // can't have font-size inherit on line above, so need to override
+ text-rendering: auto; // optimizelegibility throws things off #1094
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ transform: translate(0, 0); // ensures no half-pixel rendering in firefox
+
+}
diff --git a/Public/styles/elusive-icons/scss/_fixed-width.scss b/Public/styles/elusive-icons/scss/_fixed-width.scss
new file mode 100644
index 0000000..0458753
--- /dev/null
+++ b/Public/styles/elusive-icons/scss/_fixed-width.scss
@@ -0,0 +1,6 @@
+// Fixed Width Icons
+// -------------------------
+.#{$el-css-prefix}-fw {
+ width: (18em / 14);
+ text-align: center;
+}
diff --git a/Public/styles/elusive-icons/scss/_icons.scss b/Public/styles/elusive-icons/scss/_icons.scss
new file mode 100644
index 0000000..171ba3a
--- /dev/null
+++ b/Public/styles/elusive-icons/scss/_icons.scss
@@ -0,0 +1,307 @@
+/* Elusive Icons uses the Unicode Private Use Area (PUA) to ensure screen
+ readers do not read off random characters that represent icons */
+
+.#{$el-css-prefix}-address-book-alt:before { content: $el-var-address-book-alt; }
+.#{$el-css-prefix}-address-book:before { content: $el-var-address-book; }
+.#{$el-css-prefix}-adjust-alt:before { content: $el-var-adjust-alt; }
+.#{$el-css-prefix}-adjust:before { content: $el-var-adjust; }
+.#{$el-css-prefix}-adult:before { content: $el-var-adult; }
+.#{$el-css-prefix}-align-center:before { content: $el-var-align-center; }
+.#{$el-css-prefix}-align-justify:before { content: $el-var-align-justify; }
+.#{$el-css-prefix}-align-left:before { content: $el-var-align-left; }
+.#{$el-css-prefix}-align-right:before { content: $el-var-align-right; }
+.#{$el-css-prefix}-arrow-down:before { content: $el-var-arrow-down; }
+.#{$el-css-prefix}-arrow-left:before { content: $el-var-arrow-left; }
+.#{$el-css-prefix}-arrow-right:before { content: $el-var-arrow-right; }
+.#{$el-css-prefix}-arrow-up:before { content: $el-var-arrow-up; }
+.#{$el-css-prefix}-asl:before { content: $el-var-asl; }
+.#{$el-css-prefix}-asterisk:before { content: $el-var-asterisk; }
+.#{$el-css-prefix}-backward:before { content: $el-var-backward; }
+.#{$el-css-prefix}-ban-circle:before { content: $el-var-ban-circle; }
+.#{$el-css-prefix}-barcode:before { content: $el-var-barcode; }
+.#{$el-css-prefix}-behance:before { content: $el-var-behance; }
+.#{$el-css-prefix}-bell:before { content: $el-var-bell; }
+.#{$el-css-prefix}-blind:before { content: $el-var-blind; }
+.#{$el-css-prefix}-blogger:before { content: $el-var-blogger; }
+.#{$el-css-prefix}-bold:before { content: $el-var-bold; }
+.#{$el-css-prefix}-book:before { content: $el-var-book; }
+.#{$el-css-prefix}-bookmark-empty:before { content: $el-var-bookmark-empty; }
+.#{$el-css-prefix}-bookmark:before { content: $el-var-bookmark; }
+.#{$el-css-prefix}-braille:before { content: $el-var-braille; }
+.#{$el-css-prefix}-briefcase:before { content: $el-var-briefcase; }
+.#{$el-css-prefix}-broom:before { content: $el-var-broom; }
+.#{$el-css-prefix}-brush:before { content: $el-var-brush; }
+.#{$el-css-prefix}-bulb:before { content: $el-var-bulb; }
+.#{$el-css-prefix}-bullhorn:before { content: $el-var-bullhorn; }
+.#{$el-css-prefix}-calendar-sign:before { content: $el-var-calendar-sign; }
+.#{$el-css-prefix}-calendar:before { content: $el-var-calendar; }
+.#{$el-css-prefix}-camera:before { content: $el-var-camera; }
+.#{$el-css-prefix}-car:before { content: $el-var-car; }
+.#{$el-css-prefix}-caret-down:before { content: $el-var-caret-down; }
+.#{$el-css-prefix}-caret-left:before { content: $el-var-caret-left; }
+.#{$el-css-prefix}-caret-right:before { content: $el-var-caret-right; }
+.#{$el-css-prefix}-caret-up:before { content: $el-var-caret-up; }
+.#{$el-css-prefix}-cc:before { content: $el-var-cc; }
+.#{$el-css-prefix}-certificate:before { content: $el-var-certificate; }
+.#{$el-css-prefix}-check-empty:before { content: $el-var-check-empty; }
+.#{$el-css-prefix}-check:before { content: $el-var-check; }
+.#{$el-css-prefix}-chevron-down:before { content: $el-var-chevron-down; }
+.#{$el-css-prefix}-chevron-left:before { content: $el-var-chevron-left; }
+.#{$el-css-prefix}-chevron-right:before { content: $el-var-chevron-right; }
+.#{$el-css-prefix}-chevron-up:before { content: $el-var-chevron-up; }
+.#{$el-css-prefix}-child:before { content: $el-var-child; }
+.#{$el-css-prefix}-circle-arrow-down:before { content: $el-var-circle-arrow-down; }
+.#{$el-css-prefix}-circle-arrow-left:before { content: $el-var-circle-arrow-left; }
+.#{$el-css-prefix}-circle-arrow-right:before { content: $el-var-circle-arrow-right; }
+.#{$el-css-prefix}-circle-arrow-up:before { content: $el-var-circle-arrow-up; }
+.#{$el-css-prefix}-cloud-alt:before { content: $el-var-cloud-alt; }
+.#{$el-css-prefix}-cloud:before { content: $el-var-cloud; }
+.#{$el-css-prefix}-cog-alt:before { content: $el-var-cog-alt; }
+.#{$el-css-prefix}-cog:before { content: $el-var-cog; }
+.#{$el-css-prefix}-cogs:before { content: $el-var-cogs; }
+.#{$el-css-prefix}-comment-alt:before { content: $el-var-comment-alt; }
+.#{$el-css-prefix}-comment:before { content: $el-var-comment; }
+.#{$el-css-prefix}-compass-alt:before { content: $el-var-compass-alt; }
+.#{$el-css-prefix}-compass:before { content: $el-var-compass; }
+.#{$el-css-prefix}-credit-card:before { content: $el-var-credit-card; }
+.#{$el-css-prefix}-css:before { content: $el-var-css; }
+.#{$el-css-prefix}-dashboard:before { content: $el-var-dashboard; }
+.#{$el-css-prefix}-delicious:before { content: $el-var-delicious; }
+.#{$el-css-prefix}-deviantart:before { content: $el-var-deviantart; }
+.#{$el-css-prefix}-digg:before { content: $el-var-digg; }
+.#{$el-css-prefix}-download-alt:before { content: $el-var-download-alt; }
+.#{$el-css-prefix}-download:before { content: $el-var-download; }
+.#{$el-css-prefix}-dribbble:before { content: $el-var-dribbble; }
+.#{$el-css-prefix}-edit:before { content: $el-var-edit; }
+.#{$el-css-prefix}-eject:before { content: $el-var-eject; }
+.#{$el-css-prefix}-envelope-alt:before { content: $el-var-envelope-alt; }
+.#{$el-css-prefix}-envelope:before { content: $el-var-envelope; }
+.#{$el-css-prefix}-error-alt:before { content: $el-var-error-alt; }
+.#{$el-css-prefix}-error:before { content: $el-var-error; }
+.#{$el-css-prefix}-eur:before { content: $el-var-eur; }
+.#{$el-css-prefix}-exclamation-sign:before { content: $el-var-exclamation-sign; }
+.#{$el-css-prefix}-eye-close:before { content: $el-var-eye-close; }
+.#{$el-css-prefix}-eye-open:before { content: $el-var-eye-open; }
+.#{$el-css-prefix}-facebook:before { content: $el-var-facebook; }
+.#{$el-css-prefix}-facetime-video:before { content: $el-var-facetime-video; }
+.#{$el-css-prefix}-fast-backward:before { content: $el-var-fast-backward; }
+.#{$el-css-prefix}-fast-forward:before { content: $el-var-fast-forward; }
+.#{$el-css-prefix}-female:before { content: $el-var-female; }
+.#{$el-css-prefix}-file-alt:before { content: $el-var-file-alt; }
+.#{$el-css-prefix}-file-edit-alt:before { content: $el-var-file-edit-alt; }
+.#{$el-css-prefix}-file-edit:before { content: $el-var-file-edit; }
+.#{$el-css-prefix}-file-new-alt:before { content: $el-var-file-new-alt; }
+.#{$el-css-prefix}-file-new:before { content: $el-var-file-new; }
+.#{$el-css-prefix}-file:before { content: $el-var-file; }
+.#{$el-css-prefix}-film:before { content: $el-var-film; }
+.#{$el-css-prefix}-filter:before { content: $el-var-filter; }
+.#{$el-css-prefix}-fire:before { content: $el-var-fire; }
+.#{$el-css-prefix}-flag-alt:before { content: $el-var-flag-alt; }
+.#{$el-css-prefix}-flag:before { content: $el-var-flag; }
+.#{$el-css-prefix}-flickr:before { content: $el-var-flickr; }
+.#{$el-css-prefix}-folder-close:before { content: $el-var-folder-close; }
+.#{$el-css-prefix}-folder-open:before { content: $el-var-folder-open; }
+.#{$el-css-prefix}-folder-sign:before { content: $el-var-folder-sign; }
+.#{$el-css-prefix}-folder:before { content: $el-var-folder; }
+.#{$el-css-prefix}-font:before { content: $el-var-font; }
+.#{$el-css-prefix}-fontsize:before { content: $el-var-fontsize; }
+.#{$el-css-prefix}-fork:before { content: $el-var-fork; }
+.#{$el-css-prefix}-forward-alt:before { content: $el-var-forward-alt; }
+.#{$el-css-prefix}-forward:before { content: $el-var-forward; }
+.#{$el-css-prefix}-foursquare:before { content: $el-var-foursquare; }
+.#{$el-css-prefix}-friendfeed-rect:before { content: $el-var-friendfeed-rect; }
+.#{$el-css-prefix}-friendfeed:before { content: $el-var-friendfeed; }
+.#{$el-css-prefix}-fullscreen:before { content: $el-var-fullscreen; }
+.#{$el-css-prefix}-gbp:before { content: $el-var-gbp; }
+.#{$el-css-prefix}-gift:before { content: $el-var-gift; }
+.#{$el-css-prefix}-github-text:before { content: $el-var-github-text; }
+.#{$el-css-prefix}-github:before { content: $el-var-github; }
+.#{$el-css-prefix}-glass:before { content: $el-var-glass; }
+.#{$el-css-prefix}-glasses:before { content: $el-var-glasses; }
+.#{$el-css-prefix}-globe-alt:before { content: $el-var-globe-alt; }
+.#{$el-css-prefix}-globe:before { content: $el-var-globe; }
+.#{$el-css-prefix}-googleplus:before { content: $el-var-googleplus; }
+.#{$el-css-prefix}-graph-alt:before { content: $el-var-graph-alt; }
+.#{$el-css-prefix}-graph:before { content: $el-var-graph; }
+.#{$el-css-prefix}-group-alt:before { content: $el-var-group-alt; }
+.#{$el-css-prefix}-group:before { content: $el-var-group; }
+.#{$el-css-prefix}-guidedog:before { content: $el-var-guidedog; }
+.#{$el-css-prefix}-hand-down:before { content: $el-var-hand-down; }
+.#{$el-css-prefix}-hand-left:before { content: $el-var-hand-left; }
+.#{$el-css-prefix}-hand-right:before { content: $el-var-hand-right; }
+.#{$el-css-prefix}-hand-up:before { content: $el-var-hand-up; }
+.#{$el-css-prefix}-hdd:before { content: $el-var-hdd; }
+.#{$el-css-prefix}-headphones:before { content: $el-var-headphones; }
+.#{$el-css-prefix}-hearing-impaired:before { content: $el-var-hearing-impaired; }
+.#{$el-css-prefix}-heart-alt:before { content: $el-var-heart-alt; }
+.#{$el-css-prefix}-heart-empty:before { content: $el-var-heart-empty; }
+.#{$el-css-prefix}-heart:before { content: $el-var-heart; }
+.#{$el-css-prefix}-home-alt:before { content: $el-var-home-alt; }
+.#{$el-css-prefix}-home:before { content: $el-var-home; }
+.#{$el-css-prefix}-hourglass:before { content: $el-var-hourglass; }
+.#{$el-css-prefix}-idea-alt:before { content: $el-var-idea-alt; }
+.#{$el-css-prefix}-idea:before { content: $el-var-idea; }
+.#{$el-css-prefix}-inbox-alt:before { content: $el-var-inbox-alt; }
+.#{$el-css-prefix}-inbox-box:before { content: $el-var-inbox-box; }
+.#{$el-css-prefix}-inbox:before { content: $el-var-inbox; }
+.#{$el-css-prefix}-indent-left:before { content: $el-var-indent-left; }
+.#{$el-css-prefix}-indent-right:before { content: $el-var-indent-right; }
+.#{$el-css-prefix}-info-circle:before { content: $el-var-info-circle; }
+.#{$el-css-prefix}-instagram:before { content: $el-var-instagram; }
+.#{$el-css-prefix}-iphone-home:before { content: $el-var-iphone-home; }
+.#{$el-css-prefix}-italic:before { content: $el-var-italic; }
+.#{$el-css-prefix}-key:before { content: $el-var-key; }
+.#{$el-css-prefix}-laptop-alt:before { content: $el-var-laptop-alt; }
+.#{$el-css-prefix}-laptop:before { content: $el-var-laptop; }
+.#{$el-css-prefix}-lastfm:before { content: $el-var-lastfm; }
+.#{$el-css-prefix}-leaf:before { content: $el-var-leaf; }
+.#{$el-css-prefix}-lines:before { content: $el-var-lines; }
+.#{$el-css-prefix}-link:before { content: $el-var-link; }
+.#{$el-css-prefix}-linkedin:before { content: $el-var-linkedin; }
+.#{$el-css-prefix}-list-alt:before { content: $el-var-list-alt; }
+.#{$el-css-prefix}-list:before { content: $el-var-list; }
+.#{$el-css-prefix}-livejournal:before { content: $el-var-livejournal; }
+.#{$el-css-prefix}-lock-alt:before { content: $el-var-lock-alt; }
+.#{$el-css-prefix}-lock:before { content: $el-var-lock; }
+.#{$el-css-prefix}-magic:before { content: $el-var-magic; }
+.#{$el-css-prefix}-magnet:before { content: $el-var-magnet; }
+.#{$el-css-prefix}-male:before { content: $el-var-male; }
+.#{$el-css-prefix}-map-marker-alt:before { content: $el-var-map-marker-alt; }
+.#{$el-css-prefix}-map-marker:before { content: $el-var-map-marker; }
+.#{$el-css-prefix}-mic-alt:before { content: $el-var-mic-alt; }
+.#{$el-css-prefix}-mic:before { content: $el-var-mic; }
+.#{$el-css-prefix}-minus-sign:before { content: $el-var-minus-sign; }
+.#{$el-css-prefix}-minus:before { content: $el-var-minus; }
+.#{$el-css-prefix}-move:before { content: $el-var-move; }
+.#{$el-css-prefix}-music:before { content: $el-var-music; }
+.#{$el-css-prefix}-myspace:before { content: $el-var-myspace; }
+.#{$el-css-prefix}-network:before { content: $el-var-network; }
+.#{$el-css-prefix}-off:before { content: $el-var-off; }
+.#{$el-css-prefix}-ok-circle:before { content: $el-var-ok-circle; }
+.#{$el-css-prefix}-ok-sign:before { content: $el-var-ok-sign; }
+.#{$el-css-prefix}-ok:before { content: $el-var-ok; }
+.#{$el-css-prefix}-opensource:before { content: $el-var-opensource; }
+.#{$el-css-prefix}-paper-clip-alt:before { content: $el-var-paper-clip-alt; }
+.#{$el-css-prefix}-paper-clip:before { content: $el-var-paper-clip; }
+.#{$el-css-prefix}-path:before { content: $el-var-path; }
+.#{$el-css-prefix}-pause-alt:before { content: $el-var-pause-alt; }
+.#{$el-css-prefix}-pause:before { content: $el-var-pause; }
+.#{$el-css-prefix}-pencil-alt:before { content: $el-var-pencil-alt; }
+.#{$el-css-prefix}-pencil:before { content: $el-var-pencil; }
+.#{$el-css-prefix}-person:before { content: $el-var-person; }
+.#{$el-css-prefix}-phone-alt:before { content: $el-var-phone-alt; }
+.#{$el-css-prefix}-phone:before { content: $el-var-phone; }
+.#{$el-css-prefix}-photo-alt:before { content: $el-var-photo-alt; }
+.#{$el-css-prefix}-photo:before { content: $el-var-photo; }
+.#{$el-css-prefix}-picasa:before { content: $el-var-picasa; }
+.#{$el-css-prefix}-picture:before { content: $el-var-picture; }
+.#{$el-css-prefix}-pinterest:before { content: $el-var-pinterest; }
+.#{$el-css-prefix}-plane:before { content: $el-var-plane; }
+.#{$el-css-prefix}-play-alt:before { content: $el-var-play-alt; }
+.#{$el-css-prefix}-play-circle:before { content: $el-var-play-circle; }
+.#{$el-css-prefix}-play:before { content: $el-var-play; }
+.#{$el-css-prefix}-plurk-alt:before { content: $el-var-plurk-alt; }
+.#{$el-css-prefix}-plurk:before { content: $el-var-plurk; }
+.#{$el-css-prefix}-plus-sign:before { content: $el-var-plus-sign; }
+.#{$el-css-prefix}-plus:before { content: $el-var-plus; }
+.#{$el-css-prefix}-podcast:before { content: $el-var-podcast; }
+.#{$el-css-prefix}-print:before { content: $el-var-print; }
+.#{$el-css-prefix}-puzzle:before { content: $el-var-puzzle; }
+.#{$el-css-prefix}-qrcode:before { content: $el-var-qrcode; }
+.#{$el-css-prefix}-question-sign:before { content: $el-var-question-sign; }
+.#{$el-css-prefix}-question:before { content: $el-var-question; }
+.#{$el-css-prefix}-quote-alt:before { content: $el-var-quote-alt; }
+.#{$el-css-prefix}-quote-right-alt:before { content: $el-var-quote-right-alt; }
+.#{$el-css-prefix}-quote-right:before { content: $el-var-quote-right; }
+.#{$el-css-prefix}-quotes:before { content: $el-var-quotes; }
+.#{$el-css-prefix}-random:before { content: $el-var-random; }
+.#{$el-css-prefix}-record:before { content: $el-var-record; }
+.#{$el-css-prefix}-reddit:before { content: $el-var-reddit; }
+.#{$el-css-prefix}-redux:before { content: $el-var-redux; }
+.#{$el-css-prefix}-refresh:before { content: $el-var-refresh; }
+.#{$el-css-prefix}-remove-circle:before { content: $el-var-remove-circle; }
+.#{$el-css-prefix}-remove-sign:before { content: $el-var-remove-sign; }
+.#{$el-css-prefix}-remove:before { content: $el-var-remove; }
+.#{$el-css-prefix}-repeat-alt:before { content: $el-var-repeat-alt; }
+.#{$el-css-prefix}-repeat:before { content: $el-var-repeat; }
+.#{$el-css-prefix}-resize-full:before { content: $el-var-resize-full; }
+.#{$el-css-prefix}-resize-horizontal:before { content: $el-var-resize-horizontal; }
+.#{$el-css-prefix}-resize-small:before { content: $el-var-resize-small; }
+.#{$el-css-prefix}-resize-vertical:before { content: $el-var-resize-vertical; }
+.#{$el-css-prefix}-return-key:before { content: $el-var-return-key; }
+.#{$el-css-prefix}-retweet:before { content: $el-var-retweet; }
+.#{$el-css-prefix}-reverse-alt:before { content: $el-var-reverse-alt; }
+.#{$el-css-prefix}-road:before { content: $el-var-road; }
+.#{$el-css-prefix}-rss:before { content: $el-var-rss; }
+.#{$el-css-prefix}-scissors:before { content: $el-var-scissors; }
+.#{$el-css-prefix}-screen-alt:before { content: $el-var-screen-alt; }
+.#{$el-css-prefix}-screen:before { content: $el-var-screen; }
+.#{$el-css-prefix}-screenshot:before { content: $el-var-screenshot; }
+.#{$el-css-prefix}-search-alt:before { content: $el-var-search-alt; }
+.#{$el-css-prefix}-search:before { content: $el-var-search; }
+.#{$el-css-prefix}-share-alt:before { content: $el-var-share-alt; }
+.#{$el-css-prefix}-share:before { content: $el-var-share; }
+.#{$el-css-prefix}-shopping-cart-sign:before { content: $el-var-shopping-cart-sign; }
+.#{$el-css-prefix}-shopping-cart:before { content: $el-var-shopping-cart; }
+.#{$el-css-prefix}-signal:before { content: $el-var-signal; }
+.#{$el-css-prefix}-skype:before { content: $el-var-skype; }
+.#{$el-css-prefix}-slideshare:before { content: $el-var-slideshare; }
+.#{$el-css-prefix}-smiley-alt:before { content: $el-var-smiley-alt; }
+.#{$el-css-prefix}-smiley:before { content: $el-var-smiley; }
+.#{$el-css-prefix}-soundcloud:before { content: $el-var-soundcloud; }
+.#{$el-css-prefix}-speaker:before { content: $el-var-speaker; }
+.#{$el-css-prefix}-spotify:before { content: $el-var-spotify; }
+.#{$el-css-prefix}-stackoverflow:before { content: $el-var-stackoverflow; }
+.#{$el-css-prefix}-star-alt:before { content: $el-var-star-alt; }
+.#{$el-css-prefix}-star-empty:before { content: $el-var-star-empty; }
+.#{$el-css-prefix}-star:before { content: $el-var-star; }
+.#{$el-css-prefix}-step-backward:before { content: $el-var-step-backward; }
+.#{$el-css-prefix}-step-forward:before { content: $el-var-step-forward; }
+.#{$el-css-prefix}-stop-alt:before { content: $el-var-stop-alt; }
+.#{$el-css-prefix}-stop:before { content: $el-var-stop; }
+.#{$el-css-prefix}-stumbleupon:before { content: $el-var-stumbleupon; }
+.#{$el-css-prefix}-tag:before { content: $el-var-tag; }
+.#{$el-css-prefix}-tags:before { content: $el-var-tags; }
+.#{$el-css-prefix}-tasks:before { content: $el-var-tasks; }
+.#{$el-css-prefix}-text-height:before { content: $el-var-text-height; }
+.#{$el-css-prefix}-text-width:before { content: $el-var-text-width; }
+.#{$el-css-prefix}-th-large:before { content: $el-var-th-large; }
+.#{$el-css-prefix}-th-list:before { content: $el-var-th-list; }
+.#{$el-css-prefix}-th:before { content: $el-var-th; }
+.#{$el-css-prefix}-thumbs-down:before { content: $el-var-thumbs-down; }
+.#{$el-css-prefix}-thumbs-up:before { content: $el-var-thumbs-up; }
+.#{$el-css-prefix}-time-alt:before { content: $el-var-time-alt; }
+.#{$el-css-prefix}-time:before { content: $el-var-time; }
+.#{$el-css-prefix}-tint:before { content: $el-var-tint; }
+.#{$el-css-prefix}-torso:before { content: $el-var-torso; }
+.#{$el-css-prefix}-trash-alt:before { content: $el-var-trash-alt; }
+.#{$el-css-prefix}-trash:before { content: $el-var-trash; }
+.#{$el-css-prefix}-tumblr:before { content: $el-var-tumblr; }
+.#{$el-css-prefix}-twitter:before { content: $el-var-twitter; }
+.#{$el-css-prefix}-universal-access:before { content: $el-var-universal-access; }
+.#{$el-css-prefix}-unlock-alt:before { content: $el-var-unlock-alt; }
+.#{$el-css-prefix}-unlock:before { content: $el-var-unlock; }
+.#{$el-css-prefix}-upload:before { content: $el-var-upload; }
+.#{$el-css-prefix}-usd:before { content: $el-var-usd; }
+.#{$el-css-prefix}-user:before { content: $el-var-user; }
+.#{$el-css-prefix}-viadeo:before { content: $el-var-viadeo; }
+.#{$el-css-prefix}-video-alt:before { content: $el-var-video-alt; }
+.#{$el-css-prefix}-video-chat:before { content: $el-var-video-chat; }
+.#{$el-css-prefix}-video:before { content: $el-var-video; }
+.#{$el-css-prefix}-view-mode:before { content: $el-var-view-mode; }
+.#{$el-css-prefix}-vimeo:before { content: $el-var-vimeo; }
+.#{$el-css-prefix}-vkontakte:before { content: $el-var-vkontakte; }
+.#{$el-css-prefix}-volume-down:before { content: $el-var-volume-down; }
+.#{$el-css-prefix}-volume-off:before { content: $el-var-volume-off; }
+.#{$el-css-prefix}-volume-up:before { content: $el-var-volume-up; }
+.#{$el-css-prefix}-w3c:before { content: $el-var-w3c; }
+.#{$el-css-prefix}-warning-sign:before { content: $el-var-warning-sign; }
+.#{$el-css-prefix}-website-alt:before { content: $el-var-website-alt; }
+.#{$el-css-prefix}-website:before { content: $el-var-website; }
+.#{$el-css-prefix}-wheelchair:before { content: $el-var-wheelchair; }
+.#{$el-css-prefix}-wordpress:before { content: $el-var-wordpress; }
+.#{$el-css-prefix}-wrench-alt:before { content: $el-var-wrench-alt; }
+.#{$el-css-prefix}-wrench:before { content: $el-var-wrench; }
+.#{$el-css-prefix}-youtube:before { content: $el-var-youtube; }
+.#{$el-css-prefix}-zoom-in:before { content: $el-var-zoom-in; }
+.#{$el-css-prefix}-zoom-out:before { content: $el-var-zoom-out; }
diff --git a/Public/styles/elusive-icons/scss/_larger.scss b/Public/styles/elusive-icons/scss/_larger.scss
new file mode 100644
index 0000000..f76eab7
--- /dev/null
+++ b/Public/styles/elusive-icons/scss/_larger.scss
@@ -0,0 +1,13 @@
+// Icon Sizes
+// -------------------------
+
+/* makes the font 33% larger relative to the icon container */
+.#{$el-css-prefix}-lg {
+ font-size: (4em / 3);
+ line-height: (3em / 4);
+ vertical-align: -15%;
+}
+.#{$el-css-prefix}-2x { font-size: 2em; }
+.#{$el-css-prefix}-3x { font-size: 3em; }
+.#{$el-css-prefix}-4x { font-size: 4em; }
+.#{$el-css-prefix}-5x { font-size: 5em; }
diff --git a/Public/styles/elusive-icons/scss/_list.scss b/Public/styles/elusive-icons/scss/_list.scss
new file mode 100644
index 0000000..fefec82
--- /dev/null
+++ b/Public/styles/elusive-icons/scss/_list.scss
@@ -0,0 +1,19 @@
+// List Icons
+// -------------------------
+
+.#{$el-css-prefix}-ul {
+ padding-left: 0;
+ margin-left: $el-li-width;
+ list-style-type: none;
+ > li { position: relative; }
+}
+.#{$el-css-prefix}-li {
+ position: absolute;
+ left: -$el-li-width;
+ width: $el-li-width;
+ top: (2em / 14);
+ text-align: center;
+ &.#{$el-css-prefix}-lg {
+ left: -$el-li-width + (4em / 14);
+ }
+}
diff --git a/Public/styles/elusive-icons/scss/_mixins.scss b/Public/styles/elusive-icons/scss/_mixins.scss
new file mode 100644
index 0000000..b443823
--- /dev/null
+++ b/Public/styles/elusive-icons/scss/_mixins.scss
@@ -0,0 +1,27 @@
+// Mixins
+// --------------------------
+
+@mixin el-icon() {
+ display: inline-block;
+ font: normal normal normal #{$el-font-size-base}/1 'Elusive-Icons'; // shortening font declaration
+ font-size: inherit; // can't have font-size inherit on line above, so need to override
+ text-rendering: auto; // optimizelegibility throws things off #1094
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ transform: translate(0, 0); // ensures no half-pixel rendering in firefox
+
+}
+
+@mixin el-icon-rotate($degrees, $rotation) {
+ filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation});
+ -webkit-transform: rotate($degrees);
+ -ms-transform: rotate($degrees);
+ transform: rotate($degrees);
+}
+
+@mixin el-icon-flip($horiz, $vert, $rotation) {
+ filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation});
+ -webkit-transform: scale($horiz, $vert);
+ -ms-transform: scale($horiz, $vert);
+ transform: scale($horiz, $vert);
+}
diff --git a/Public/styles/elusive-icons/scss/_path.scss b/Public/styles/elusive-icons/scss/_path.scss
new file mode 100644
index 0000000..4989ef2
--- /dev/null
+++ b/Public/styles/elusive-icons/scss/_path.scss
@@ -0,0 +1,14 @@
+/* FONT PATH
+ * -------------------------- */
+
+@font-face {
+ font-family: 'Elusive-Icons';
+ src: url('#{$el-font-path}/elusiveicons-webfont.eot?v=#{$el-version}');
+ src: url('#{$el-font-path}/elusiveicons-webfont.eot?#iefix&v=#{$el-version}') format('embedded-opentype'),
+ //url('#{$el-font-path}/elusiveicons-webfont.woff2?v=#{$el-version}') format('woff2'),
+ url('#{$el-font-path}/elusiveicons-webfont.woff?v=#{$el-version}') format('woff'),
+ url('#{$el-font-path}/elusiveicons-webfont.ttf?v=#{$el-version}') format('truetype'),
+ url('#{$el-font-path}/elusiveicons-webfont.svg?v=#{$el-version}#elusiveiconsregular') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
diff --git a/Public/styles/elusive-icons/scss/_rotated-flipped.scss b/Public/styles/elusive-icons/scss/_rotated-flipped.scss
new file mode 100644
index 0000000..8248161
--- /dev/null
+++ b/Public/styles/elusive-icons/scss/_rotated-flipped.scss
@@ -0,0 +1,20 @@
+// Rotated & Flipped Icons
+// -------------------------
+
+.#{$el-css-prefix}-rotate-90 { @include el-icon-rotate(90deg, 1); }
+.#{$el-css-prefix}-rotate-180 { @include el-icon-rotate(180deg, 2); }
+.#{$el-css-prefix}-rotate-270 { @include el-icon-rotate(270deg, 3); }
+
+.#{$el-css-prefix}-flip-horizontal { @include el-icon-flip(-1, 1, 0); }
+.#{$el-css-prefix}-flip-vertical { @include el-icon-flip(1, -1, 2); }
+
+// Hook for IE8-9
+// -------------------------
+
+:root .#{$el-css-prefix}-rotate-90,
+:root .#{$el-css-prefix}-rotate-180,
+:root .#{$el-css-prefix}-rotate-270,
+:root .#{$el-css-prefix}-flip-horizontal,
+:root .#{$el-css-prefix}-flip-vertical {
+ filter: none;
+}
diff --git a/Public/styles/elusive-icons/scss/_stacked.scss b/Public/styles/elusive-icons/scss/_stacked.scss
new file mode 100644
index 0000000..68d84bb
--- /dev/null
+++ b/Public/styles/elusive-icons/scss/_stacked.scss
@@ -0,0 +1,20 @@
+// Stacked Icons
+// -------------------------
+
+.#{$el-css-prefix}-stack {
+ position: relative;
+ display: inline-block;
+ width: 2em;
+ height: 2em;
+ line-height: 2em;
+ vertical-align: middle;
+}
+.#{$el-css-prefix}-stack-1x, .#{$el-css-prefix}-stack-2x {
+ position: absolute;
+ left: 0;
+ width: 100%;
+ text-align: center;
+}
+.#{$el-css-prefix}-stack-1x { line-height: inherit; }
+.#{$el-css-prefix}-stack-2x { font-size: 2em; }
+.#{$el-css-prefix}-inverse { color: $el-inverse; }
diff --git a/Public/styles/elusive-icons/scss/_variables.scss b/Public/styles/elusive-icons/scss/_variables.scss
new file mode 100644
index 0000000..30a0aec
--- /dev/null
+++ b/Public/styles/elusive-icons/scss/_variables.scss
@@ -0,0 +1,317 @@
+// Variables
+// --------------------------
+
+$el-font-path: "../fonts" !default;
+$el-font-size-base: 14px !default;
+//$el-font-path: "//netdna.bootstrapcdn.com/elusive-icons/2.0.0/fonts" !default; // for referencing Bootstrap CDN font files directly
+$el-css-prefix: el !default;
+$el-version: "2.0.0" !default;
+$el-border-color: #eee !default;
+$el-inverse: #fff !default;
+$el-li-width: (30em / 14) !default;
+
+$el-var-address-book: "\f102";
+$el-var-address-book-alt: "\f101";
+$el-var-adjust: "\f104";
+$el-var-adjust-alt: "\f103";
+$el-var-adult: "\f105";
+$el-var-align-center: "\f106";
+$el-var-align-justify: "\f107";
+$el-var-align-left: "\f108";
+$el-var-align-right: "\f109";
+$el-var-arrow-down: "\f10a";
+$el-var-arrow-left: "\f10b";
+$el-var-arrow-right: "\f10c";
+$el-var-arrow-up: "\f10d";
+$el-var-asl: "\f10e";
+$el-var-asterisk: "\f10f";
+$el-var-backward: "\f110";
+$el-var-ban-circle: "\f111";
+$el-var-barcode: "\f112";
+$el-var-behance: "\f113";
+$el-var-bell: "\f114";
+$el-var-blind: "\f115";
+$el-var-blogger: "\f116";
+$el-var-bold: "\f117";
+$el-var-book: "\f118";
+$el-var-bookmark: "\f11a";
+$el-var-bookmark-empty: "\f119";
+$el-var-braille: "\f11b";
+$el-var-briefcase: "\f11c";
+$el-var-broom: "\f11d";
+$el-var-brush: "\f11e";
+$el-var-bulb: "\f11f";
+$el-var-bullhorn: "\f120";
+$el-var-calendar: "\f122";
+$el-var-calendar-sign: "\f121";
+$el-var-camera: "\f123";
+$el-var-car: "\f124";
+$el-var-caret-down: "\f125";
+$el-var-caret-left: "\f126";
+$el-var-caret-right: "\f127";
+$el-var-caret-up: "\f128";
+$el-var-cc: "\f129";
+$el-var-certificate: "\f12a";
+$el-var-check: "\f12c";
+$el-var-check-empty: "\f12b";
+$el-var-chevron-down: "\f12d";
+$el-var-chevron-left: "\f12e";
+$el-var-chevron-right: "\f12f";
+$el-var-chevron-up: "\f130";
+$el-var-child: "\f131";
+$el-var-circle-arrow-down: "\f132";
+$el-var-circle-arrow-left: "\f133";
+$el-var-circle-arrow-right: "\f134";
+$el-var-circle-arrow-up: "\f135";
+$el-var-cloud: "\f137";
+$el-var-cloud-alt: "\f136";
+$el-var-cog: "\f139";
+$el-var-cog-alt: "\f138";
+$el-var-cogs: "\f13a";
+$el-var-comment: "\f13c";
+$el-var-comment-alt: "\f13b";
+$el-var-compass: "\f13e";
+$el-var-compass-alt: "\f13d";
+$el-var-credit-card: "\f13f";
+$el-var-css: "\f140";
+$el-var-dashboard: "\f141";
+$el-var-delicious: "\f142";
+$el-var-deviantart: "\f143";
+$el-var-digg: "\f144";
+$el-var-download: "\f146";
+$el-var-download-alt: "\f145";
+$el-var-dribbble: "\f147";
+$el-var-edit: "\f148";
+$el-var-eject: "\f149";
+$el-var-envelope: "\f14b";
+$el-var-envelope-alt: "\f14a";
+$el-var-error: "\f14d";
+$el-var-error-alt: "\f14c";
+$el-var-eur: "\f14e";
+$el-var-exclamation-sign: "\f14f";
+$el-var-eye-close: "\f150";
+$el-var-eye-open: "\f151";
+$el-var-facebook: "\f152";
+$el-var-facetime-video: "\f153";
+$el-var-fast-backward: "\f154";
+$el-var-fast-forward: "\f155";
+$el-var-female: "\f156";
+$el-var-file: "\f15c";
+$el-var-file-alt: "\f157";
+$el-var-file-edit: "\f159";
+$el-var-file-edit-alt: "\f158";
+$el-var-file-new: "\f15b";
+$el-var-file-new-alt: "\f15a";
+$el-var-film: "\f15d";
+$el-var-filter: "\f15e";
+$el-var-fire: "\f15f";
+$el-var-flag: "\f161";
+$el-var-flag-alt: "\f160";
+$el-var-flickr: "\f162";
+$el-var-folder: "\f166";
+$el-var-folder-close: "\f163";
+$el-var-folder-open: "\f164";
+$el-var-folder-sign: "\f165";
+$el-var-font: "\f167";
+$el-var-fontsize: "\f168";
+$el-var-fork: "\f169";
+$el-var-forward: "\f16b";
+$el-var-forward-alt: "\f16a";
+$el-var-foursquare: "\f16c";
+$el-var-friendfeed: "\f16e";
+$el-var-friendfeed-rect: "\f16d";
+$el-var-fullscreen: "\f16f";
+$el-var-gbp: "\f170";
+$el-var-gift: "\f171";
+$el-var-github: "\f173";
+$el-var-github-text: "\f172";
+$el-var-glass: "\f174";
+$el-var-glasses: "\f175";
+$el-var-globe: "\f177";
+$el-var-globe-alt: "\f176";
+$el-var-googleplus: "\f178";
+$el-var-graph: "\f17a";
+$el-var-graph-alt: "\f179";
+$el-var-group: "\f17c";
+$el-var-group-alt: "\f17b";
+$el-var-guidedog: "\f17d";
+$el-var-hand-down: "\f17e";
+$el-var-hand-left: "\f17f";
+$el-var-hand-right: "\f180";
+$el-var-hand-up: "\f181";
+$el-var-hdd: "\f182";
+$el-var-headphones: "\f183";
+$el-var-hearing-impaired: "\f184";
+$el-var-heart: "\f187";
+$el-var-heart-alt: "\f185";
+$el-var-heart-empty: "\f186";
+$el-var-home: "\f189";
+$el-var-home-alt: "\f188";
+$el-var-hourglass: "\f18a";
+$el-var-idea: "\f18c";
+$el-var-idea-alt: "\f18b";
+$el-var-inbox: "\f18f";
+$el-var-inbox-alt: "\f18d";
+$el-var-inbox-box: "\f18e";
+$el-var-indent-left: "\f190";
+$el-var-indent-right: "\f191";
+$el-var-info-circle: "\f192";
+$el-var-instagram: "\f193";
+$el-var-iphone-home: "\f194";
+$el-var-italic: "\f195";
+$el-var-key: "\f196";
+$el-var-laptop: "\f198";
+$el-var-laptop-alt: "\f197";
+$el-var-lastfm: "\f199";
+$el-var-leaf: "\f19a";
+$el-var-lines: "\f19b";
+$el-var-link: "\f19c";
+$el-var-linkedin: "\f19d";
+$el-var-list: "\f19f";
+$el-var-list-alt: "\f19e";
+$el-var-livejournal: "\f1a0";
+$el-var-lock: "\f1a2";
+$el-var-lock-alt: "\f1a1";
+$el-var-magic: "\f1a3";
+$el-var-magnet: "\f1a4";
+$el-var-male: "\f1a5";
+$el-var-map-marker: "\f1a7";
+$el-var-map-marker-alt: "\f1a6";
+$el-var-mic: "\f1a9";
+$el-var-mic-alt: "\f1a8";
+$el-var-minus: "\f1ab";
+$el-var-minus-sign: "\f1aa";
+$el-var-move: "\f1ac";
+$el-var-music: "\f1ad";
+$el-var-myspace: "\f1ae";
+$el-var-network: "\f1af";
+$el-var-off: "\f1b0";
+$el-var-ok: "\f1b3";
+$el-var-ok-circle: "\f1b1";
+$el-var-ok-sign: "\f1b2";
+$el-var-opensource: "\f1b4";
+$el-var-paper-clip: "\f1b6";
+$el-var-paper-clip-alt: "\f1b5";
+$el-var-path: "\f1b7";
+$el-var-pause: "\f1b9";
+$el-var-pause-alt: "\f1b8";
+$el-var-pencil: "\f1bb";
+$el-var-pencil-alt: "\f1ba";
+$el-var-person: "\f1bc";
+$el-var-phone: "\f1be";
+$el-var-phone-alt: "\f1bd";
+$el-var-photo: "\f1c0";
+$el-var-photo-alt: "\f1bf";
+$el-var-picasa: "\f1c1";
+$el-var-picture: "\f1c2";
+$el-var-pinterest: "\f1c3";
+$el-var-plane: "\f1c4";
+$el-var-play: "\f1c7";
+$el-var-play-alt: "\f1c5";
+$el-var-play-circle: "\f1c6";
+$el-var-plurk: "\f1c9";
+$el-var-plurk-alt: "\f1c8";
+$el-var-plus: "\f1cb";
+$el-var-plus-sign: "\f1ca";
+$el-var-podcast: "\f1cc";
+$el-var-print: "\f1cd";
+$el-var-puzzle: "\f1ce";
+$el-var-qrcode: "\f1cf";
+$el-var-question: "\f1d1";
+$el-var-question-sign: "\f1d0";
+$el-var-quote-alt: "\f1d2";
+$el-var-quote-right: "\f1d4";
+$el-var-quote-right-alt: "\f1d3";
+$el-var-quotes: "\f1d5";
+$el-var-random: "\f1d6";
+$el-var-record: "\f1d7";
+$el-var-reddit: "\f1d8";
+$el-var-redux: "\f1d9";
+$el-var-refresh: "\f1da";
+$el-var-remove: "\f1dd";
+$el-var-remove-circle: "\f1db";
+$el-var-remove-sign: "\f1dc";
+$el-var-repeat: "\f1df";
+$el-var-repeat-alt: "\f1de";
+$el-var-resize-full: "\f1e0";
+$el-var-resize-horizontal: "\f1e1";
+$el-var-resize-small: "\f1e2";
+$el-var-resize-vertical: "\f1e3";
+$el-var-return-key: "\f1e4";
+$el-var-retweet: "\f1e5";
+$el-var-reverse-alt: "\f1e6";
+$el-var-road: "\f1e7";
+$el-var-rss: "\f1e8";
+$el-var-scissors: "\f1e9";
+$el-var-screen: "\f1eb";
+$el-var-screen-alt: "\f1ea";
+$el-var-screenshot: "\f1ec";
+$el-var-search: "\f1ee";
+$el-var-search-alt: "\f1ed";
+$el-var-share: "\f1f0";
+$el-var-share-alt: "\f1ef";
+$el-var-shopping-cart: "\f1f2";
+$el-var-shopping-cart-sign: "\f1f1";
+$el-var-signal: "\f1f3";
+$el-var-skype: "\f1f4";
+$el-var-slideshare: "\f1f5";
+$el-var-smiley: "\f1f7";
+$el-var-smiley-alt: "\f1f6";
+$el-var-soundcloud: "\f1f8";
+$el-var-speaker: "\f1f9";
+$el-var-spotify: "\f1fa";
+$el-var-stackoverflow: "\f1fb";
+$el-var-star: "\f1fe";
+$el-var-star-alt: "\f1fc";
+$el-var-star-empty: "\f1fd";
+$el-var-step-backward: "\f1ff";
+$el-var-step-forward: "\f200";
+$el-var-stop: "\f202";
+$el-var-stop-alt: "\f201";
+$el-var-stumbleupon: "\f203";
+$el-var-tag: "\f204";
+$el-var-tags: "\f205";
+$el-var-tasks: "\f206";
+$el-var-text-height: "\f207";
+$el-var-text-width: "\f208";
+$el-var-th: "\f20b";
+$el-var-th-large: "\f209";
+$el-var-th-list: "\f20a";
+$el-var-thumbs-down: "\f20c";
+$el-var-thumbs-up: "\f20d";
+$el-var-time: "\f20f";
+$el-var-time-alt: "\f20e";
+$el-var-tint: "\f210";
+$el-var-torso: "\f211";
+$el-var-trash: "\f213";
+$el-var-trash-alt: "\f212";
+$el-var-tumblr: "\f214";
+$el-var-twitter: "\f215";
+$el-var-universal-access: "\f216";
+$el-var-unlock: "\f218";
+$el-var-unlock-alt: "\f217";
+$el-var-upload: "\f219";
+$el-var-usd: "\f21a";
+$el-var-user: "\f21b";
+$el-var-viadeo: "\f21c";
+$el-var-video: "\f21f";
+$el-var-video-alt: "\f21d";
+$el-var-video-chat: "\f21e";
+$el-var-view-mode: "\f220";
+$el-var-vimeo: "\f221";
+$el-var-vkontakte: "\f222";
+$el-var-volume-down: "\f223";
+$el-var-volume-off: "\f224";
+$el-var-volume-up: "\f225";
+$el-var-w3c: "\f226";
+$el-var-warning-sign: "\f227";
+$el-var-website: "\f229";
+$el-var-website-alt: "\f228";
+$el-var-wheelchair: "\f22a";
+$el-var-wordpress: "\f22b";
+$el-var-wrench: "\f22d";
+$el-var-wrench-alt: "\f22c";
+$el-var-youtube: "\f22e";
+$el-var-zoom-in: "\f22f";
+$el-var-zoom-out: "\f230";
+
diff --git a/Public/styles/elusive-icons/scss/elusive-icons.scss b/Public/styles/elusive-icons/scss/elusive-icons.scss
new file mode 100644
index 0000000..c3bb7ba
--- /dev/null
+++ b/Public/styles/elusive-icons/scss/elusive-icons.scss
@@ -0,0 +1,17 @@
+/*!
+ * Elusive Icons 2.0.0 by @ReduxFramework - http://elusiveicons.com - @reduxframework
+ * License - http://elusiveicons.com/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */
+
+@import "variables";
+@import "mixins";
+@import "path";
+@import "core";
+@import "larger";
+@import "fixed-width";
+@import "list";
+@import "bordered-pulled";
+@import "animated";
+@import "rotated-flipped";
+@import "stacked";
+@import "icons";
diff --git a/Public/styles/milligram.css b/Public/styles/milligram.css
new file mode 100644
index 0000000..8118dee
--- /dev/null
+++ b/Public/styles/milligram.css
@@ -0,0 +1,635 @@
+/*!
+ * Milligram v1.4.1
+ * https://milligram.io
+ *
+ * Copyright (c) 2020 CJ Patoilo
+ * Licensed under the MIT license
+ */
+
+*,
+*:after,
+*:before {
+ box-sizing: inherit;
+}
+
+html {
+ box-sizing: border-box;
+ font-size: 62.5%;
+}
+
+body {
+ color: #606c76;
+ font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
+ font-size: 1.6em;
+ font-weight: 300;
+ letter-spacing: .01em;
+ line-height: 1.6;
+}
+
+blockquote {
+ border-left: 0.3rem solid #d1d1d1;
+ margin-left: 0;
+ margin-right: 0;
+ padding: 1rem 1.5rem;
+}
+
+blockquote *:last-child {
+ margin-bottom: 0;
+}
+
+.button,
+button,
+input[type='button'],
+input[type='reset'],
+input[type='submit'] {
+ background-color: #9b4dca;
+ border: 0.1rem solid #9b4dca;
+ border-radius: .4rem;
+ color: #fff;
+ cursor: pointer;
+ display: inline-block;
+ font-size: 1.1rem;
+ font-weight: 700;
+ height: 3.8rem;
+ letter-spacing: .1rem;
+ line-height: 3.8rem;
+ padding: 0 3.0rem;
+ text-align: center;
+ text-decoration: none;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
+.button:focus, .button:hover,
+button:focus,
+button:hover,
+input[type='button']:focus,
+input[type='button']:hover,
+input[type='reset']:focus,
+input[type='reset']:hover,
+input[type='submit']:focus,
+input[type='submit']:hover {
+ background-color: #606c76;
+ border-color: #606c76;
+ color: #fff;
+ outline: 0;
+}
+
+.button[disabled],
+button[disabled],
+input[type='button'][disabled],
+input[type='reset'][disabled],
+input[type='submit'][disabled] {
+ cursor: default;
+ opacity: .5;
+}
+
+.button[disabled]:focus, .button[disabled]:hover,
+button[disabled]:focus,
+button[disabled]:hover,
+input[type='button'][disabled]:focus,
+input[type='button'][disabled]:hover,
+input[type='reset'][disabled]:focus,
+input[type='reset'][disabled]:hover,
+input[type='submit'][disabled]:focus,
+input[type='submit'][disabled]:hover {
+ background-color: #9b4dca;
+ border-color: #9b4dca;
+}
+
+.button.button-outline,
+button.button-outline,
+input[type='button'].button-outline,
+input[type='reset'].button-outline,
+input[type='submit'].button-outline {
+ background-color: transparent;
+ color: #9b4dca;
+}
+
+.button.button-outline:focus, .button.button-outline:hover,
+button.button-outline:focus,
+button.button-outline:hover,
+input[type='button'].button-outline:focus,
+input[type='button'].button-outline:hover,
+input[type='reset'].button-outline:focus,
+input[type='reset'].button-outline:hover,
+input[type='submit'].button-outline:focus,
+input[type='submit'].button-outline:hover {
+ background-color: transparent;
+ border-color: #606c76;
+ color: #606c76;
+}
+
+.button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover,
+button.button-outline[disabled]:focus,
+button.button-outline[disabled]:hover,
+input[type='button'].button-outline[disabled]:focus,
+input[type='button'].button-outline[disabled]:hover,
+input[type='reset'].button-outline[disabled]:focus,
+input[type='reset'].button-outline[disabled]:hover,
+input[type='submit'].button-outline[disabled]:focus,
+input[type='submit'].button-outline[disabled]:hover {
+ border-color: inherit;
+ color: #9b4dca;
+}
+
+.button.button-clear,
+button.button-clear,
+input[type='button'].button-clear,
+input[type='reset'].button-clear,
+input[type='submit'].button-clear {
+ background-color: transparent;
+ border-color: transparent;
+ color: #9b4dca;
+}
+
+.button.button-clear:focus, .button.button-clear:hover,
+button.button-clear:focus,
+button.button-clear:hover,
+input[type='button'].button-clear:focus,
+input[type='button'].button-clear:hover,
+input[type='reset'].button-clear:focus,
+input[type='reset'].button-clear:hover,
+input[type='submit'].button-clear:focus,
+input[type='submit'].button-clear:hover {
+ background-color: transparent;
+ border-color: transparent;
+ color: #606c76;
+}
+
+.button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover,
+button.button-clear[disabled]:focus,
+button.button-clear[disabled]:hover,
+input[type='button'].button-clear[disabled]:focus,
+input[type='button'].button-clear[disabled]:hover,
+input[type='reset'].button-clear[disabled]:focus,
+input[type='reset'].button-clear[disabled]:hover,
+input[type='submit'].button-clear[disabled]:focus,
+input[type='submit'].button-clear[disabled]:hover {
+ color: #9b4dca;
+}
+
+code {
+ background: #f4f5f6;
+ border-radius: .4rem;
+ font-size: 86%;
+ margin: 0 .2rem;
+ padding: .2rem .5rem;
+ white-space: nowrap;
+}
+
+pre {
+ background: #f4f5f6;
+ border-left: 0.3rem solid #9b4dca;
+ overflow-y: hidden;
+}
+
+pre > code {
+ border-radius: 0;
+ display: block;
+ padding: 1rem 1.5rem;
+ white-space: pre;
+}
+
+hr {
+ border: 0;
+ border-top: 0.1rem solid #f4f5f6;
+ margin: 3.0rem 0;
+}
+
+input[type='color'],
+input[type='date'],
+input[type='datetime'],
+input[type='datetime-local'],
+input[type='email'],
+input[type='month'],
+input[type='number'],
+input[type='password'],
+input[type='search'],
+input[type='tel'],
+input[type='text'],
+input[type='url'],
+input[type='week'],
+input:not([type]),
+textarea,
+select {
+ -webkit-appearance: none;
+ background-color: transparent;
+ border: 0.1rem solid #d1d1d1;
+ border-radius: .4rem;
+ box-shadow: none;
+ box-sizing: inherit;
+ height: 3.8rem;
+ padding: .6rem 1.0rem .7rem;
+ width: 100%;
+}
+
+input[type='color']:focus,
+input[type='date']:focus,
+input[type='datetime']:focus,
+input[type='datetime-local']:focus,
+input[type='email']:focus,
+input[type='month']:focus,
+input[type='number']:focus,
+input[type='password']:focus,
+input[type='search']:focus,
+input[type='tel']:focus,
+input[type='text']:focus,
+input[type='url']:focus,
+input[type='week']:focus,
+input:not([type]):focus,
+textarea:focus,
+select:focus {
+ border-color: #9b4dca;
+ outline: 0;
+}
+
+select {
+ background: url('data:image/svg+xml;utf8, ') center right no-repeat;
+ padding-right: 3.0rem;
+}
+
+select:focus {
+ background-image: url('data:image/svg+xml;utf8, ');
+}
+
+select[multiple] {
+ background: none;
+ height: auto;
+}
+
+textarea {
+ min-height: 6.5rem;
+}
+
+label,
+legend {
+ display: block;
+ font-size: 1.6rem;
+ font-weight: 700;
+ margin-bottom: .5rem;
+}
+
+fieldset {
+ border-width: 0;
+ padding: 0;
+}
+
+input[type='checkbox'],
+input[type='radio'] {
+ display: inline;
+}
+
+.label-inline {
+ display: inline-block;
+ font-weight: normal;
+ margin-left: .5rem;
+}
+
+.container {
+ margin: 0 auto;
+ max-width: 112.0rem;
+ padding: 0 2.0rem;
+ position: relative;
+ width: 100%;
+}
+
+.row {
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ width: 100%;
+}
+
+.row.row-no-padding {
+ padding: 0;
+}
+
+.row.row-no-padding > .column {
+ padding: 0;
+}
+
+.row.row-wrap {
+ flex-wrap: wrap;
+}
+
+.row.row-top {
+ align-items: flex-start;
+}
+
+.row.row-bottom {
+ align-items: flex-end;
+}
+
+.row.row-center {
+ align-items: center;
+}
+
+.row.row-stretch {
+ align-items: stretch;
+}
+
+.row.row-baseline {
+ align-items: baseline;
+}
+
+.row .column {
+ display: block;
+ flex: 1 1 auto;
+ margin-left: 0;
+ max-width: 100%;
+ width: 100%;
+}
+
+.row .column.column-offset-10 {
+ margin-left: 10%;
+}
+
+.row .column.column-offset-20 {
+ margin-left: 20%;
+}
+
+.row .column.column-offset-25 {
+ margin-left: 25%;
+}
+
+.row .column.column-offset-33, .row .column.column-offset-34 {
+ margin-left: 33.3333%;
+}
+
+.row .column.column-offset-40 {
+ margin-left: 40%;
+}
+
+.row .column.column-offset-50 {
+ margin-left: 50%;
+}
+
+.row .column.column-offset-60 {
+ margin-left: 60%;
+}
+
+.row .column.column-offset-66, .row .column.column-offset-67 {
+ margin-left: 66.6666%;
+}
+
+.row .column.column-offset-75 {
+ margin-left: 75%;
+}
+
+.row .column.column-offset-80 {
+ margin-left: 80%;
+}
+
+.row .column.column-offset-90 {
+ margin-left: 90%;
+}
+
+.row .column.column-10 {
+ flex: 0 0 10%;
+ max-width: 10%;
+}
+
+.row .column.column-20 {
+ flex: 0 0 20%;
+ max-width: 20%;
+}
+
+.row .column.column-25 {
+ flex: 0 0 25%;
+ max-width: 25%;
+}
+
+.row .column.column-33, .row .column.column-34 {
+ flex: 0 0 33.3333%;
+ max-width: 33.3333%;
+}
+
+.row .column.column-40 {
+ flex: 0 0 40%;
+ max-width: 40%;
+}
+
+.row .column.column-50 {
+ flex: 0 0 50%;
+ max-width: 50%;
+}
+
+.row .column.column-60 {
+ flex: 0 0 60%;
+ max-width: 60%;
+}
+
+.row .column.column-66, .row .column.column-67 {
+ flex: 0 0 66.6666%;
+ max-width: 66.6666%;
+}
+
+.row .column.column-75 {
+ flex: 0 0 75%;
+ max-width: 75%;
+}
+
+.row .column.column-80 {
+ flex: 0 0 80%;
+ max-width: 80%;
+}
+
+.row .column.column-90 {
+ flex: 0 0 90%;
+ max-width: 90%;
+}
+
+.row .column .column-top {
+ align-self: flex-start;
+}
+
+.row .column .column-bottom {
+ align-self: flex-end;
+}
+
+.row .column .column-center {
+ align-self: center;
+}
+
+@media (min-width: 40rem) {
+ .row {
+ flex-direction: row;
+ margin-left: -1.0rem;
+ width: calc(100% + 2.0rem);
+ }
+ .row .column {
+ margin-bottom: inherit;
+ padding: 0 1.0rem;
+ }
+}
+
+a {
+ color: #9b4dca;
+ text-decoration: none;
+}
+
+a:focus, a:hover {
+ color: #606c76;
+}
+
+dl,
+ol,
+ul {
+ list-style: none;
+ margin-top: 0;
+ padding-left: 0;
+}
+
+dl dl,
+dl ol,
+dl ul,
+ol dl,
+ol ol,
+ol ul,
+ul dl,
+ul ol,
+ul ul {
+ font-size: 90%;
+ margin: 1.5rem 0 1.5rem 3.0rem;
+}
+
+ol {
+ list-style: decimal inside;
+}
+
+ul {
+ list-style: circle inside;
+}
+
+.button,
+button,
+dd,
+dt,
+li {
+ margin-bottom: 1.0rem;
+}
+
+fieldset,
+input,
+select,
+textarea {
+ margin-bottom: 1.5rem;
+}
+
+blockquote,
+dl,
+figure,
+form,
+ol,
+p,
+pre,
+table,
+ul {
+ margin-bottom: 2.5rem;
+}
+
+table {
+ border-spacing: 0;
+ display: block;
+ overflow-x: auto;
+ text-align: left;
+ width: 100%;
+}
+
+td,
+th {
+ border-bottom: 0.1rem solid #e1e1e1;
+ padding: 1.2rem 1.5rem;
+}
+
+td:first-child,
+th:first-child {
+ padding-left: 0;
+}
+
+td:last-child,
+th:last-child {
+ padding-right: 0;
+}
+
+@media (min-width: 40rem) {
+ table {
+ display: table;
+ overflow-x: initial;
+ }
+}
+
+b,
+strong {
+ font-weight: bold;
+}
+
+p {
+ margin-top: 0;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-weight: 300;
+ letter-spacing: -.1rem;
+ margin-bottom: 2.0rem;
+ margin-top: 0;
+}
+
+h1 {
+ font-size: 4.6rem;
+ line-height: 1.2;
+}
+
+h2 {
+ font-size: 3.6rem;
+ line-height: 1.25;
+}
+
+h3 {
+ font-size: 2.8rem;
+ line-height: 1.3;
+}
+
+h4 {
+ font-size: 2.2rem;
+ letter-spacing: -.08rem;
+ line-height: 1.35;
+}
+
+h5 {
+ font-size: 1.8rem;
+ letter-spacing: -.05rem;
+ line-height: 1.5;
+}
+
+h6 {
+ font-size: 1.6rem;
+ letter-spacing: 0;
+ line-height: 1.4;
+}
+
+img {
+ max-width: 100%;
+}
+
+.clearfix:after {
+ clear: both;
+ content: ' ';
+ display: table;
+}
+
+.float-left {
+ float: left;
+}
+
+.float-right {
+ float: right;
+}
+
+/*# sourceMappingURL=milligram.css.map */
\ No newline at end of file
diff --git a/Public/styles/milligram.css.map b/Public/styles/milligram.css.map
new file mode 100644
index 0000000..3195274
--- /dev/null
+++ b/Public/styles/milligram.css.map
@@ -0,0 +1 @@
+{"version":3,"sources":["milligram.css"],"names":[],"mappings":"AAAA;;;EAGE,mBAAmB;AACrB;;AAEA;EACE,sBAAsB;EACtB,gBAAgB;AAClB;;AAEA;EACE,cAAc;EACd,yEAAyE;EACzE,gBAAgB;EAChB,gBAAgB;EAChB,qBAAqB;EACrB,gBAAgB;AAClB;;AAEA;EACE,iCAAiC;EACjC,cAAc;EACd,eAAe;EACf,oBAAoB;AACtB;;AAEA;EACE,gBAAgB;AAClB;;AAEA;;;;;EAKE,yBAAyB;EACzB,4BAA4B;EAC5B,oBAAoB;EACpB,WAAW;EACX,eAAe;EACf,qBAAqB;EACrB,iBAAiB;EACjB,gBAAgB;EAChB,cAAc;EACd,qBAAqB;EACrB,mBAAmB;EACnB,iBAAiB;EACjB,kBAAkB;EAClB,qBAAqB;EACrB,yBAAyB;EACzB,mBAAmB;AACrB;;AAEA;;;;;;;;;EASE,yBAAyB;EACzB,qBAAqB;EACrB,WAAW;EACX,UAAU;AACZ;;AAEA;;;;;EAKE,eAAe;EACf,WAAW;AACb;;AAEA;;;;;;;;;EASE,yBAAyB;EACzB,qBAAqB;AACvB;;AAEA;;;;;EAKE,6BAA6B;EAC7B,cAAc;AAChB;;AAEA;;;;;;;;;EASE,6BAA6B;EAC7B,qBAAqB;EACrB,cAAc;AAChB;;AAEA;;;;;;;;;EASE,qBAAqB;EACrB,cAAc;AAChB;;AAEA;;;;;EAKE,6BAA6B;EAC7B,yBAAyB;EACzB,cAAc;AAChB;;AAEA;;;;;;;;;EASE,6BAA6B;EAC7B,yBAAyB;EACzB,cAAc;AAChB;;AAEA;;;;;;;;;EASE,cAAc;AAChB;;AAEA;EACE,mBAAmB;EACnB,oBAAoB;EACpB,cAAc;EACd,eAAe;EACf,oBAAoB;EACpB,mBAAmB;AACrB;;AAEA;EACE,mBAAmB;EACnB,iCAAiC;EACjC,kBAAkB;AACpB;;AAEA;EACE,gBAAgB;EAChB,cAAc;EACd,oBAAoB;EACpB,gBAAgB;AAClB;;AAEA;EACE,SAAS;EACT,gCAAgC;EAChC,gBAAgB;AAClB;;AAEA;;;;;;;;;;;;;;;;EAgBE,wBAAwB;EACxB,6BAA6B;EAC7B,4BAA4B;EAC5B,oBAAoB;EACpB,gBAAgB;EAChB,mBAAmB;EACnB,cAAc;EACd,2BAA2B;EAC3B,WAAW;AACb;;AAEA;;;;;;;;;;;;;;;;EAgBE,qBAAqB;EACrB,UAAU;AACZ;;AAEA;EACE,uLAAuL;EACvL,qBAAqB;AACvB;;AAEA;EACE,sKAAsK;AACxK;;AAEA;EACE,gBAAgB;EAChB,YAAY;AACd;;AAEA;EACE,kBAAkB;AACpB;;AAEA;;EAEE,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,oBAAoB;AACtB;;AAEA;EACE,eAAe;EACf,UAAU;AACZ;;AAEA;;EAEE,eAAe;AACjB;;AAEA;EACE,qBAAqB;EACrB,mBAAmB;EACnB,kBAAkB;AACpB;;AAEA;EACE,cAAc;EACd,mBAAmB;EACnB,iBAAiB;EACjB,kBAAkB;EAClB,WAAW;AACb;;AAEA;EACE,aAAa;EACb,sBAAsB;EACtB,UAAU;EACV,WAAW;AACb;;AAEA;EACE,UAAU;AACZ;;AAEA;EACE,UAAU;AACZ;;AAEA;EACE,eAAe;AACjB;;AAEA;EACE,uBAAuB;AACzB;;AAEA;EACE,qBAAqB;AACvB;;AAEA;EACE,mBAAmB;AACrB;;AAEA;EACE,oBAAoB;AACtB;;AAEA;EACE,qBAAqB;AACvB;;AAEA;EACE,cAAc;EACd,cAAc;EACd,cAAc;EACd,eAAe;EACf,WAAW;AACb;;AAEA;EACE,gBAAgB;AAClB;;AAEA;EACE,gBAAgB;AAClB;;AAEA;EACE,gBAAgB;AAClB;;AAEA;EACE,qBAAqB;AACvB;;AAEA;EACE,gBAAgB;AAClB;;AAEA;EACE,gBAAgB;AAClB;;AAEA;EACE,gBAAgB;AAClB;;AAEA;EACE,qBAAqB;AACvB;;AAEA;EACE,gBAAgB;AAClB;;AAEA;EACE,gBAAgB;AAClB;;AAEA;EACE,gBAAgB;AAClB;;AAEA;EACE,aAAa;EACb,cAAc;AAChB;;AAEA;EACE,aAAa;EACb,cAAc;AAChB;;AAEA;EACE,aAAa;EACb,cAAc;AAChB;;AAEA;EACE,kBAAkB;EAClB,mBAAmB;AACrB;;AAEA;EACE,aAAa;EACb,cAAc;AAChB;;AAEA;EACE,aAAa;EACb,cAAc;AAChB;;AAEA;EACE,aAAa;EACb,cAAc;AAChB;;AAEA;EACE,kBAAkB;EAClB,mBAAmB;AACrB;;AAEA;EACE,aAAa;EACb,cAAc;AAChB;;AAEA;EACE,aAAa;EACb,cAAc;AAChB;;AAEA;EACE,aAAa;EACb,cAAc;AAChB;;AAEA;EACE,sBAAsB;AACxB;;AAEA;EACE,oBAAoB;AACtB;;AAEA;EACE,kBAAkB;AACpB;;AAEA;EACE;IACE,mBAAmB;IACnB,oBAAoB;IACpB,0BAA0B;EAC5B;EACA;IACE,sBAAsB;IACtB,iBAAiB;EACnB;AACF;;AAEA;EACE,cAAc;EACd,qBAAqB;AACvB;;AAEA;EACE,cAAc;AAChB;;AAEA;;;EAGE,gBAAgB;EAChB,aAAa;EACb,eAAe;AACjB;;AAEA;;;;;;;;;EASE,cAAc;EACd,8BAA8B;AAChC;;AAEA;EACE,0BAA0B;AAC5B;;AAEA;EACE,yBAAyB;AAC3B;;AAEA;;;;;EAKE,qBAAqB;AACvB;;AAEA;;;;EAIE,qBAAqB;AACvB;;AAEA;;;;;;;;;EASE,qBAAqB;AACvB;;AAEA;EACE,iBAAiB;EACjB,cAAc;EACd,gBAAgB;EAChB,gBAAgB;EAChB,WAAW;AACb;;AAEA;;EAEE,mCAAmC;EACnC,sBAAsB;AACxB;;AAEA;;EAEE,eAAe;AACjB;;AAEA;;EAEE,gBAAgB;AAClB;;AAEA;EACE;IACE,cAAc;IACd,mBAAmB;EACrB;AACF;;AAEA;;EAEE,iBAAiB;AACnB;;AAEA;EACE,aAAa;AACf;;AAEA;;;;;;EAME,gBAAgB;EAChB,sBAAsB;EACtB,qBAAqB;EACrB,aAAa;AACf;;AAEA;EACE,iBAAiB;EACjB,gBAAgB;AAClB;;AAEA;EACE,iBAAiB;EACjB,iBAAiB;AACnB;;AAEA;EACE,iBAAiB;EACjB,gBAAgB;AAClB;;AAEA;EACE,iBAAiB;EACjB,uBAAuB;EACvB,iBAAiB;AACnB;;AAEA;EACE,iBAAiB;EACjB,uBAAuB;EACvB,gBAAgB;AAClB;;AAEA;EACE,iBAAiB;EACjB,iBAAiB;EACjB,gBAAgB;AAClB;;AAEA;EACE,eAAe;AACjB;;AAEA;EACE,WAAW;EACX,YAAY;EACZ,cAAc;AAChB;;AAEA;EACE,WAAW;AACb;;AAEA;EACE,YAAY;AACd","file":"milligram.css","sourcesContent":["*,\n*:after,\n*:before {\n box-sizing: inherit;\n}\n\nhtml {\n box-sizing: border-box;\n font-size: 62.5%;\n}\n\nbody {\n color: #606c76;\n font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;\n font-size: 1.6em;\n font-weight: 300;\n letter-spacing: .01em;\n line-height: 1.6;\n}\n\nblockquote {\n border-left: 0.3rem solid #d1d1d1;\n margin-left: 0;\n margin-right: 0;\n padding: 1rem 1.5rem;\n}\n\nblockquote *:last-child {\n margin-bottom: 0;\n}\n\n.button,\nbutton,\ninput[type='button'],\ninput[type='reset'],\ninput[type='submit'] {\n background-color: #9b4dca;\n border: 0.1rem solid #9b4dca;\n border-radius: .4rem;\n color: #fff;\n cursor: pointer;\n display: inline-block;\n font-size: 1.1rem;\n font-weight: 700;\n height: 3.8rem;\n letter-spacing: .1rem;\n line-height: 3.8rem;\n padding: 0 3.0rem;\n text-align: center;\n text-decoration: none;\n text-transform: uppercase;\n white-space: nowrap;\n}\n\n.button:focus, .button:hover,\nbutton:focus,\nbutton:hover,\ninput[type='button']:focus,\ninput[type='button']:hover,\ninput[type='reset']:focus,\ninput[type='reset']:hover,\ninput[type='submit']:focus,\ninput[type='submit']:hover {\n background-color: #606c76;\n border-color: #606c76;\n color: #fff;\n outline: 0;\n}\n\n.button[disabled],\nbutton[disabled],\ninput[type='button'][disabled],\ninput[type='reset'][disabled],\ninput[type='submit'][disabled] {\n cursor: default;\n opacity: .5;\n}\n\n.button[disabled]:focus, .button[disabled]:hover,\nbutton[disabled]:focus,\nbutton[disabled]:hover,\ninput[type='button'][disabled]:focus,\ninput[type='button'][disabled]:hover,\ninput[type='reset'][disabled]:focus,\ninput[type='reset'][disabled]:hover,\ninput[type='submit'][disabled]:focus,\ninput[type='submit'][disabled]:hover {\n background-color: #9b4dca;\n border-color: #9b4dca;\n}\n\n.button.button-outline,\nbutton.button-outline,\ninput[type='button'].button-outline,\ninput[type='reset'].button-outline,\ninput[type='submit'].button-outline {\n background-color: transparent;\n color: #9b4dca;\n}\n\n.button.button-outline:focus, .button.button-outline:hover,\nbutton.button-outline:focus,\nbutton.button-outline:hover,\ninput[type='button'].button-outline:focus,\ninput[type='button'].button-outline:hover,\ninput[type='reset'].button-outline:focus,\ninput[type='reset'].button-outline:hover,\ninput[type='submit'].button-outline:focus,\ninput[type='submit'].button-outline:hover {\n background-color: transparent;\n border-color: #606c76;\n color: #606c76;\n}\n\n.button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover,\nbutton.button-outline[disabled]:focus,\nbutton.button-outline[disabled]:hover,\ninput[type='button'].button-outline[disabled]:focus,\ninput[type='button'].button-outline[disabled]:hover,\ninput[type='reset'].button-outline[disabled]:focus,\ninput[type='reset'].button-outline[disabled]:hover,\ninput[type='submit'].button-outline[disabled]:focus,\ninput[type='submit'].button-outline[disabled]:hover {\n border-color: inherit;\n color: #9b4dca;\n}\n\n.button.button-clear,\nbutton.button-clear,\ninput[type='button'].button-clear,\ninput[type='reset'].button-clear,\ninput[type='submit'].button-clear {\n background-color: transparent;\n border-color: transparent;\n color: #9b4dca;\n}\n\n.button.button-clear:focus, .button.button-clear:hover,\nbutton.button-clear:focus,\nbutton.button-clear:hover,\ninput[type='button'].button-clear:focus,\ninput[type='button'].button-clear:hover,\ninput[type='reset'].button-clear:focus,\ninput[type='reset'].button-clear:hover,\ninput[type='submit'].button-clear:focus,\ninput[type='submit'].button-clear:hover {\n background-color: transparent;\n border-color: transparent;\n color: #606c76;\n}\n\n.button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover,\nbutton.button-clear[disabled]:focus,\nbutton.button-clear[disabled]:hover,\ninput[type='button'].button-clear[disabled]:focus,\ninput[type='button'].button-clear[disabled]:hover,\ninput[type='reset'].button-clear[disabled]:focus,\ninput[type='reset'].button-clear[disabled]:hover,\ninput[type='submit'].button-clear[disabled]:focus,\ninput[type='submit'].button-clear[disabled]:hover {\n color: #9b4dca;\n}\n\ncode {\n background: #f4f5f6;\n border-radius: .4rem;\n font-size: 86%;\n margin: 0 .2rem;\n padding: .2rem .5rem;\n white-space: nowrap;\n}\n\npre {\n background: #f4f5f6;\n border-left: 0.3rem solid #9b4dca;\n overflow-y: hidden;\n}\n\npre > code {\n border-radius: 0;\n display: block;\n padding: 1rem 1.5rem;\n white-space: pre;\n}\n\nhr {\n border: 0;\n border-top: 0.1rem solid #f4f5f6;\n margin: 3.0rem 0;\n}\n\ninput[type='color'],\ninput[type='date'],\ninput[type='datetime'],\ninput[type='datetime-local'],\ninput[type='email'],\ninput[type='month'],\ninput[type='number'],\ninput[type='password'],\ninput[type='search'],\ninput[type='tel'],\ninput[type='text'],\ninput[type='url'],\ninput[type='week'],\ninput:not([type]),\ntextarea,\nselect {\n -webkit-appearance: none;\n background-color: transparent;\n border: 0.1rem solid #d1d1d1;\n border-radius: .4rem;\n box-shadow: none;\n box-sizing: inherit;\n height: 3.8rem;\n padding: .6rem 1.0rem .7rem;\n width: 100%;\n}\n\ninput[type='color']:focus,\ninput[type='date']:focus,\ninput[type='datetime']:focus,\ninput[type='datetime-local']:focus,\ninput[type='email']:focus,\ninput[type='month']:focus,\ninput[type='number']:focus,\ninput[type='password']:focus,\ninput[type='search']:focus,\ninput[type='tel']:focus,\ninput[type='text']:focus,\ninput[type='url']:focus,\ninput[type='week']:focus,\ninput:not([type]):focus,\ntextarea:focus,\nselect:focus {\n border-color: #9b4dca;\n outline: 0;\n}\n\nselect {\n background: url('data:image/svg+xml;utf8, ') center right no-repeat;\n padding-right: 3.0rem;\n}\n\nselect:focus {\n background-image: url('data:image/svg+xml;utf8, ');\n}\n\nselect[multiple] {\n background: none;\n height: auto;\n}\n\ntextarea {\n min-height: 6.5rem;\n}\n\nlabel,\nlegend {\n display: block;\n font-size: 1.6rem;\n font-weight: 700;\n margin-bottom: .5rem;\n}\n\nfieldset {\n border-width: 0;\n padding: 0;\n}\n\ninput[type='checkbox'],\ninput[type='radio'] {\n display: inline;\n}\n\n.label-inline {\n display: inline-block;\n font-weight: normal;\n margin-left: .5rem;\n}\n\n.container {\n margin: 0 auto;\n max-width: 112.0rem;\n padding: 0 2.0rem;\n position: relative;\n width: 100%;\n}\n\n.row {\n display: flex;\n flex-direction: column;\n padding: 0;\n width: 100%;\n}\n\n.row.row-no-padding {\n padding: 0;\n}\n\n.row.row-no-padding > .column {\n padding: 0;\n}\n\n.row.row-wrap {\n flex-wrap: wrap;\n}\n\n.row.row-top {\n align-items: flex-start;\n}\n\n.row.row-bottom {\n align-items: flex-end;\n}\n\n.row.row-center {\n align-items: center;\n}\n\n.row.row-stretch {\n align-items: stretch;\n}\n\n.row.row-baseline {\n align-items: baseline;\n}\n\n.row .column {\n display: block;\n flex: 1 1 auto;\n margin-left: 0;\n max-width: 100%;\n width: 100%;\n}\n\n.row .column.column-offset-10 {\n margin-left: 10%;\n}\n\n.row .column.column-offset-20 {\n margin-left: 20%;\n}\n\n.row .column.column-offset-25 {\n margin-left: 25%;\n}\n\n.row .column.column-offset-33, .row .column.column-offset-34 {\n margin-left: 33.3333%;\n}\n\n.row .column.column-offset-40 {\n margin-left: 40%;\n}\n\n.row .column.column-offset-50 {\n margin-left: 50%;\n}\n\n.row .column.column-offset-60 {\n margin-left: 60%;\n}\n\n.row .column.column-offset-66, .row .column.column-offset-67 {\n margin-left: 66.6666%;\n}\n\n.row .column.column-offset-75 {\n margin-left: 75%;\n}\n\n.row .column.column-offset-80 {\n margin-left: 80%;\n}\n\n.row .column.column-offset-90 {\n margin-left: 90%;\n}\n\n.row .column.column-10 {\n flex: 0 0 10%;\n max-width: 10%;\n}\n\n.row .column.column-20 {\n flex: 0 0 20%;\n max-width: 20%;\n}\n\n.row .column.column-25 {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.row .column.column-33, .row .column.column-34 {\n flex: 0 0 33.3333%;\n max-width: 33.3333%;\n}\n\n.row .column.column-40 {\n flex: 0 0 40%;\n max-width: 40%;\n}\n\n.row .column.column-50 {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.row .column.column-60 {\n flex: 0 0 60%;\n max-width: 60%;\n}\n\n.row .column.column-66, .row .column.column-67 {\n flex: 0 0 66.6666%;\n max-width: 66.6666%;\n}\n\n.row .column.column-75 {\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.row .column.column-80 {\n flex: 0 0 80%;\n max-width: 80%;\n}\n\n.row .column.column-90 {\n flex: 0 0 90%;\n max-width: 90%;\n}\n\n.row .column .column-top {\n align-self: flex-start;\n}\n\n.row .column .column-bottom {\n align-self: flex-end;\n}\n\n.row .column .column-center {\n align-self: center;\n}\n\n@media (min-width: 40rem) {\n .row {\n flex-direction: row;\n margin-left: -1.0rem;\n width: calc(100% + 2.0rem);\n }\n .row .column {\n margin-bottom: inherit;\n padding: 0 1.0rem;\n }\n}\n\na {\n color: #9b4dca;\n text-decoration: none;\n}\n\na:focus, a:hover {\n color: #606c76;\n}\n\ndl,\nol,\nul {\n list-style: none;\n margin-top: 0;\n padding-left: 0;\n}\n\ndl dl,\ndl ol,\ndl ul,\nol dl,\nol ol,\nol ul,\nul dl,\nul ol,\nul ul {\n font-size: 90%;\n margin: 1.5rem 0 1.5rem 3.0rem;\n}\n\nol {\n list-style: decimal inside;\n}\n\nul {\n list-style: circle inside;\n}\n\n.button,\nbutton,\ndd,\ndt,\nli {\n margin-bottom: 1.0rem;\n}\n\nfieldset,\ninput,\nselect,\ntextarea {\n margin-bottom: 1.5rem;\n}\n\nblockquote,\ndl,\nfigure,\nform,\nol,\np,\npre,\ntable,\nul {\n margin-bottom: 2.5rem;\n}\n\ntable {\n border-spacing: 0;\n display: block;\n overflow-x: auto;\n text-align: left;\n width: 100%;\n}\n\ntd,\nth {\n border-bottom: 0.1rem solid #e1e1e1;\n padding: 1.2rem 1.5rem;\n}\n\ntd:first-child,\nth:first-child {\n padding-left: 0;\n}\n\ntd:last-child,\nth:last-child {\n padding-right: 0;\n}\n\n@media (min-width: 40rem) {\n table {\n display: table;\n overflow-x: initial;\n }\n}\n\nb,\nstrong {\n font-weight: bold;\n}\n\np {\n margin-top: 0;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n font-weight: 300;\n letter-spacing: -.1rem;\n margin-bottom: 2.0rem;\n margin-top: 0;\n}\n\nh1 {\n font-size: 4.6rem;\n line-height: 1.2;\n}\n\nh2 {\n font-size: 3.6rem;\n line-height: 1.25;\n}\n\nh3 {\n font-size: 2.8rem;\n line-height: 1.3;\n}\n\nh4 {\n font-size: 2.2rem;\n letter-spacing: -.08rem;\n line-height: 1.35;\n}\n\nh5 {\n font-size: 1.8rem;\n letter-spacing: -.05rem;\n line-height: 1.5;\n}\n\nh6 {\n font-size: 1.6rem;\n letter-spacing: 0;\n line-height: 1.4;\n}\n\nimg {\n max-width: 100%;\n}\n\n.clearfix:after {\n clear: both;\n content: ' ';\n display: table;\n}\n\n.float-left {\n float: left;\n}\n\n.float-right {\n float: right;\n}\n"]}
\ No newline at end of file
diff --git a/Public/styles/milligram.min.css b/Public/styles/milligram.min.css
new file mode 100644
index 0000000..5e8955c
--- /dev/null
+++ b/Public/styles/milligram.min.css
@@ -0,0 +1,3 @@
+*,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;letter-spacing:.01em;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#9b4dca;border:0.1rem solid #9b4dca;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#9b4dca;border-color:#9b4dca}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#9b4dca}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#9b4dca}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#9b4dca}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#9b4dca}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #9b4dca;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='color'],input[type='date'],input[type='datetime'],input[type='datetime-local'],input[type='email'],input[type='month'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],input[type='week'],input:not([type]),textarea,select{-webkit-appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem .7rem;width:100%}input[type='color']:focus,input[type='date']:focus,input[type='datetime']:focus,input[type='datetime-local']:focus,input[type='email']:focus,input[type='month']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,input[type='week']:focus,input:not([type]):focus,textarea:focus,select:focus{border-color:#9b4dca;outline:0}select{background:url('data:image/svg+xml;utf8, ') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8, ')}select[multiple]{background:none;height:auto}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.container{margin:0 auto;max-width:112.0rem;padding:0 2.0rem;position:relative;width:100%}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-40{margin-left:40%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-60{margin-left:60%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#9b4dca;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;display:block;overflow-x:auto;text-align:left;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}@media (min-width: 40rem){table{display:table;overflow-x:initial}}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}
+
+/*# sourceMappingURL=milligram.min.css.map */
\ No newline at end of file
diff --git a/Public/styles/milligram.min.css.map b/Public/styles/milligram.min.css.map
new file mode 100644
index 0000000..009da6a
--- /dev/null
+++ b/Public/styles/milligram.min.css.map
@@ -0,0 +1 @@
+{"version":3,"sources":["milligram.min.css"],"names":[],"mappings":"AAAA,mBAAmB,kBAAkB,CAAC,KAAK,qBAAqB,CAAC,eAAe,CAAC,KAAK,aAAa,CAAC,wEAAwE,CAAC,eAAe,CAAC,eAAe,CAAC,oBAAoB,CAAC,eAAe,CAAC,WAAW,gCAAgC,CAAC,aAAa,CAAC,cAAc,CAAC,mBAAmB,CAAC,wBAAwB,eAAe,CAAC,6EAA6E,wBAAwB,CAAC,2BAA2B,CAAC,mBAAmB,CAAC,UAAU,CAAC,cAAc,CAAC,oBAAoB,CAAC,gBAAgB,CAAC,eAAe,CAAC,aAAa,CAAC,oBAAoB,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,iBAAiB,CAAC,oBAAoB,CAAC,wBAAwB,CAAC,kBAAkB,CAAC,sNAAsN,wBAAwB,CAAC,oBAAoB,CAAC,UAAU,CAAC,SAAS,CAAC,+HAA+H,cAAc,CAAC,UAAU,CAAC,0TAA0T,wBAAwB,CAAC,oBAAoB,CAAC,wJAAwJ,4BAA4B,CAAC,aAAa,CAAC,4WAA4W,4BAA4B,CAAC,oBAAoB,CAAC,aAAa,CAAC,gdAAgd,oBAAoB,CAAC,aAAa,CAAC,8IAA8I,4BAA4B,CAAC,wBAAwB,CAAC,aAAa,CAAC,wVAAwV,4BAA4B,CAAC,wBAAwB,CAAC,aAAa,CAAC,4bAA4b,aAAa,CAAC,KAAK,kBAAkB,CAAC,mBAAmB,CAAC,aAAa,CAAC,cAAc,CAAC,mBAAmB,CAAC,kBAAkB,CAAC,IAAI,kBAAkB,CAAC,gCAAgC,CAAC,iBAAiB,CAAC,SAAS,eAAe,CAAC,aAAa,CAAC,mBAAmB,CAAC,eAAe,CAAC,GAAG,QAAQ,CAAC,+BAA+B,CAAC,eAAe,CAAC,gTAAgT,uBAAuB,CAAC,4BAA4B,CAAC,2BAA2B,CAAC,mBAAmB,CAAC,eAAe,CAAC,kBAAkB,CAAC,aAAa,CAAC,0BAA0B,CAAC,UAAU,CAAC,gZAAgZ,oBAAoB,CAAC,SAAS,CAAC,OAAO,sLAAsL,CAAC,oBAAoB,CAAC,aAAa,qKAAqK,CAAC,iBAAiB,eAAe,CAAC,WAAW,CAAC,SAAS,iBAAiB,CAAC,aAAa,aAAa,CAAC,gBAAgB,CAAC,eAAe,CAAC,mBAAmB,CAAC,SAAS,cAAc,CAAC,SAAS,CAAC,2CAA2C,cAAc,CAAC,cAAc,oBAAoB,CAAC,kBAAkB,CAAC,iBAAiB,CAAC,WAAW,aAAa,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,iBAAiB,CAAC,UAAU,CAAC,KAAK,YAAY,CAAC,qBAAqB,CAAC,SAAS,CAAC,UAAU,CAAC,oBAAoB,SAAS,CAAC,4BAA4B,SAAS,CAAC,cAAc,cAAc,CAAC,aAAa,sBAAsB,CAAC,gBAAgB,oBAAoB,CAAC,gBAAgB,kBAAkB,CAAC,iBAAiB,mBAAmB,CAAC,kBAAkB,oBAAoB,CAAC,aAAa,aAAa,CAAC,aAAa,CAAC,aAAa,CAAC,cAAc,CAAC,UAAU,CAAC,8BAA8B,eAAe,CAAC,8BAA8B,eAAe,CAAC,8BAA8B,eAAe,CAAC,4DAA4D,oBAAoB,CAAC,8BAA8B,eAAe,CAAC,8BAA8B,eAAe,CAAC,8BAA8B,eAAe,CAAC,4DAA4D,oBAAoB,CAAC,8BAA8B,eAAe,CAAC,8BAA8B,eAAe,CAAC,8BAA8B,eAAe,CAAC,uBAAuB,YAAY,CAAC,aAAa,CAAC,uBAAuB,YAAY,CAAC,aAAa,CAAC,uBAAuB,YAAY,CAAC,aAAa,CAAC,8CAA8C,iBAAiB,CAAC,kBAAkB,CAAC,uBAAuB,YAAY,CAAC,aAAa,CAAC,uBAAuB,YAAY,CAAC,aAAa,CAAC,uBAAuB,YAAY,CAAC,aAAa,CAAC,8CAA8C,iBAAiB,CAAC,kBAAkB,CAAC,uBAAuB,YAAY,CAAC,aAAa,CAAC,uBAAuB,YAAY,CAAC,aAAa,CAAC,uBAAuB,YAAY,CAAC,aAAa,CAAC,yBAAyB,qBAAqB,CAAC,4BAA4B,mBAAmB,CAAC,4BAA4B,iBAAiB,CAAC,0BAA0B,KAAK,kBAAkB,CAAC,mBAAmB,CAAC,yBAAyB,CAAC,aAAa,qBAAqB,CAAC,gBAAgB,CAAC,CAAC,EAAE,aAAa,CAAC,oBAAoB,CAAC,gBAAgB,aAAa,CAAC,SAAS,eAAe,CAAC,YAAY,CAAC,cAAc,CAAC,sDAAsD,aAAa,CAAC,6BAA6B,CAAC,GAAG,yBAAyB,CAAC,GAAG,wBAAwB,CAAC,wBAAwB,oBAAoB,CAAC,+BAA+B,oBAAoB,CAAC,4CAA4C,oBAAoB,CAAC,MAAM,gBAAgB,CAAC,aAAa,CAAC,eAAe,CAAC,eAAe,CAAC,UAAU,CAAC,MAAM,kCAAkC,CAAC,qBAAqB,CAAC,8BAA8B,cAAc,CAAC,4BAA4B,eAAe,CAAC,0BAA0B,MAAM,aAAa,CAAC,kBAAkB,CAAC,CAAC,SAAS,gBAAgB,CAAC,EAAE,YAAY,CAAC,kBAAkB,eAAe,CAAC,qBAAqB,CAAC,oBAAoB,CAAC,YAAY,CAAC,GAAG,gBAAgB,CAAC,eAAe,CAAC,GAAG,gBAAgB,CAAC,gBAAgB,CAAC,GAAG,gBAAgB,CAAC,eAAe,CAAC,GAAG,gBAAgB,CAAC,sBAAsB,CAAC,gBAAgB,CAAC,GAAG,gBAAgB,CAAC,sBAAsB,CAAC,eAAe,CAAC,GAAG,gBAAgB,CAAC,gBAAgB,CAAC,eAAe,CAAC,IAAI,cAAc,CAAC,gBAAgB,UAAU,CAAC,WAAW,CAAC,aAAa,CAAC,YAAY,UAAU,CAAC,aAAa,WAAW","file":"milligram.min.css","sourcesContent":["*,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;letter-spacing:.01em;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#9b4dca;border:0.1rem solid #9b4dca;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#9b4dca;border-color:#9b4dca}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#9b4dca}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#9b4dca}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#9b4dca}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#9b4dca}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #9b4dca;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='color'],input[type='date'],input[type='datetime'],input[type='datetime-local'],input[type='email'],input[type='month'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],input[type='week'],input:not([type]),textarea,select{-webkit-appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem .7rem;width:100%}input[type='color']:focus,input[type='date']:focus,input[type='datetime']:focus,input[type='datetime-local']:focus,input[type='email']:focus,input[type='month']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,input[type='week']:focus,input:not([type]):focus,textarea:focus,select:focus{border-color:#9b4dca;outline:0}select{background:url('data:image/svg+xml;utf8, ') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8, ')}select[multiple]{background:none;height:auto}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.container{margin:0 auto;max-width:112.0rem;padding:0 2.0rem;position:relative;width:100%}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-40{margin-left:40%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-60{margin-left:60%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#9b4dca;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;display:block;overflow-x:auto;text-align:left;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}@media (min-width: 40rem){table{display:table;overflow-x:initial}}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}\n"]}
\ No newline at end of file
diff --git a/Public/styles/normalize.css b/Public/styles/normalize.css
new file mode 100644
index 0000000..192eb9c
--- /dev/null
+++ b/Public/styles/normalize.css
@@ -0,0 +1,349 @@
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+ ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+
+html {
+ line-height: 1.15; /* 1 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+ ========================================================================== */
+
+/**
+ * Remove the margin in all browsers.
+ */
+
+body {
+ margin: 0;
+}
+
+/**
+ * Render the `main` element consistently in IE.
+ */
+
+main {
+ display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+ box-sizing: content-box; /* 1 */
+ height: 0; /* 1 */
+ overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+ border-bottom: none; /* 1 */
+ text-decoration: underline; /* 2 */
+ text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+
+img {
+ border-style: none;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: inherit; /* 1 */
+ font-size: 100%; /* 1 */
+ line-height: 1.15; /* 1 */
+ margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+ overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+ text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+ -webkit-appearance: button;
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+ border-style: none;
+ padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+ outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+ padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ * `fieldset` elements in all browsers.
+ */
+
+legend {
+ box-sizing: border-box; /* 1 */
+ color: inherit; /* 2 */
+ display: table; /* 1 */
+ max-width: 100%; /* 1 */
+ padding: 0; /* 3 */
+ white-space: normal; /* 1 */
+}
+
+/**
+ * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+ vertical-align: baseline;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+ -webkit-appearance: button; /* 1 */
+ font: inherit; /* 2 */
+}
+
+/* Interactive
+ ========================================================================== */
+
+/*
+ * Add the correct display in Edge, IE 10+, and Firefox.
+ */
+
+details {
+ display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+ display: list-item;
+}
+
+/* Misc
+ ========================================================================== */
+
+/**
+ * Add the correct display in IE 10+.
+ */
+
+template {
+ display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+
+[hidden] {
+ display: none;
+}
diff --git a/Public/styles/style.css b/Public/styles/style.css
new file mode 100644
index 0000000..c2da572
--- /dev/null
+++ b/Public/styles/style.css
@@ -0,0 +1,133 @@
+html, body {
+ margin: 10px;
+ font-family: 'Catamaran', sans-serif;
+}
+header > nav {
+ display: block
+}
+header > nav > ul {
+ list-style: none;
+}
+header h1 {
+ margin: 20px 0px;
+}
+header .logo {
+ height: 0.9em;
+}
+header > nav > ul > li {
+ float: left;
+ margin: 5px;
+ padding: 5px;
+}
+header > nav > ul:first-child > li:first-child {
+ margin-left: -5px;
+}
+header > nav > ul.float-right li {
+ float: right;
+}
+header.container {
+ margin-bottom: 20px;
+}
+ul.articles {
+ list-style: none;
+}
+
+ul.articles > li {
+ padding: 1em 0px;
+}
+
+ul.articles li > .title i.el {
+ margin-right: 4px;
+}
+
+ul.articles li > .summary {
+ padding: 8px 0px;
+}
+
+ul.articles li > .publishedAt, ul.articles li > .author{
+ font-size: 0.8em;
+}
+
+ul.articles li > .author a.twitter-handle{
+ line-height: 2em;
+ height: 2em;
+ padding: 0px 5px;
+ text-transform: unset;
+ margin-left: 4px;
+}
+
+main nav.posts-filter ul {
+ list-style: none
+}
+
+main nav.posts-filter ul li{
+ float: left;
+ list-style: none
+}
+
+main nav.posts-filter ul li a.button{
+ padding: 0px 10px;
+ line-height: 2.25em;
+ height: 2.25em;
+ margin-right: 1em;
+}
+
+ul.articles li > .social-share > ul{
+ list-style: none;
+ display: inline-block;
+ margin: 5px;
+}
+
+ul.articles li > .social-share > ul li{
+ display: inline-block;
+ padding: 0;
+}
+
+ul.articles li > .social-share > ul > li > a{
+ line-height: 2em;
+ height: 2em;
+ padding: 0px 5px;
+}
+
+ul.articles li > ul.podcast-players {
+ list-style: none;
+ margin: 0;
+}
+
+ul.articles li > ul.podcast-players > li {
+ display: inline-block;
+ margin-left: 4px;
+}
+
+ul.articles li > ul.podcast-players > li > a {
+ border: solid 1px;
+ height: 3em;
+ display: inline-block;
+ padding: 4px;
+ border-radius: 8px;
+ width: 10em;
+}
+
+ul.articles li > ul.podcast-players > li img {
+ object-fit: contain;
+ height: 100%;
+}
+
+ul.articles li > ul.podcast-players > li img+div {
+ display: inline-block;
+ margin-left: 4px;
+}
+
+ul.articles li > ul.podcast-players > li img+div > div:first-child {
+ font-size: 0.7em;
+}
+/*
+ul.articles li > ul.podcast-players > li {
+ height: 2em;
+ display: inline-block;
+}
+
+
+
+
+*/
diff --git a/Public/test.html b/Public/test.html
new file mode 100644
index 0000000..6f7317f
--- /dev/null
+++ b/Public/test.html
@@ -0,0 +1,804 @@
+
+
+
+ OrchardNest - Swift Articles and News
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ OrchardNest
+
+
+
+
Swift Articles and News
+
+
+
+
+
+
+
+
+
+
+
+
+ RSS Feed Not Found - Wed Aug 05 2020
+
+
+ 2020-08-05 17:42:16 +0000
+ RSS feed not found - create a new RSS feed at RSS.app.
+
+
+
+
+
+
+ Adding a closure as a target to UIButton and other controls in Swift
+
+
+ 2020-08-05 17:15:14 +0000
+ The target-action pattern is used in combination with user interface controls as a callback to a user event. Whenever a button is pressed on a target, its action will be called. The fact that the method is not defined close to the control...
+
+
+
+
+
+
+ 95: “We’re not lawyers”
+
+
+ 2020-08-05 17:00:00 +0000
+ What differentiates server-side and client-side development, Tim Cook’s testimony in front of the US Congress, the new iMac, Rambo’s secret new project, and more on John’s not-so-secret use of SwiftUI to build games.
+Sponsored by...
+
+
+
+
+
+
+
+
+
+ My approach to setting up Core Data stack
+
+
+ 2020-08-05 15:40:19 +0000
+ I got asked on the Hacking With Swift forums about my approach to configuring Core Data outside AppDelegate and decided to share my solution here. I personally think Core Data code has no business to be inside AppDelegate and it would be...
+
+
+
+
+
+
+ RRPagingCollectionView
+
+
+ 2020-08-05 14:44:13 +0000
+ The simplest way to make your collection view paginationMore info about RRPagingCollectionView
+
+
+
+
+
+
+ AppCode 2020.2 is Here With Initial Swift Package Manager Support, the Change Signature Refactoring For Swift, Performance Improvements, and More!
+
+
+ 2020-08-05 14:28:45 +0000
+ Introducing our second update this year – AppCode 2020.2! Download AppCode 2020.2 Initial Swift Package Manager Support Swift Package Manager provides an easy way to manage project dependencies in Xcode projects, and its rate of adoption...
+
+
+
+
+
+
+ Why Mobile Customer Feedback is More Important than Ever for Finance Brands
+
+
+ 2020-08-05 13:21:45 +0000
+ Mobile-first finance companies are disrupting the market left and right – especially in light of the COVID-19 pandemic. While we saw DAU (daily active users) stay pretty consistent for traditional banking apps since the pandemic hit,...
+
+
+
+
+
+
+ 358: Design and Venture Capital
+
+
+ 2020-08-05 12:00:29 +0000
+ This week, we caught up with Bobby Goodlatte and Josh Williams, two designers-turned-investors who recently announced Form Capital, an early-stage venture fund. In this episode we talk about the path to starting a fund, whether founders...
+
+
+
+
+
+
+
+
+
+ Working with Timers in Swift
+
+
+ 2020-08-05 11:00:28 +0000
+ Timers are super handy in Swift, from creating repeating tasks to scheduling work with a delay. This article explains how to create a timer in Swift. Time's ticking – let's get to it!
+The post Working with Timers in Swift appeared first on...
+
+
+
+
+
+
+ Common Reasons for Background Tasks to Fail in iOS
+
+
+ 2020-08-05 11:00:00 +0000
+ Apple introduced modern background tasks last year on iOS 13. These new APIs have been out for a little over year (counting the beta period). Many developers have tried to adopt them to moderate success. Many of them have found them to be...
+
+
+
+
+
+
+ Make RVM's Ruby Available to Emacs Shell Commands
+
+
+ 2020-08-05 09:10:15 +0000
+ No matter if you use exec-path-from-shell or not, Emacs will not be able to know your RVM-managed Ruby information. This drove me crazy. Most Emacs shell commands are invoked in an “inferior” mode, aka a “dumb” shell. This includes M-!,...
+
+
+
+
+
+
+ A Guide to Beacon Technology in 2020
+
+
+ 2020-08-05 08:40:00 +0000
+ Need a fully-fledged navigation system for under a couple hundred bucks?
+Take a stroll through a park or forest and chances are you'll hear a lot of bird songs and chirping. Birds generally aren’t trying to call for anyone specific –...
+
+
+
+
+
+
+ Why Is the PDF Format so Well Supported on Apple Platforms?
+
+
+ 2020-08-05 08:00:00 +0000
+ The PDF format has been well supported on Apple platforms for a long time, and to better understand why, let’s take a trip down memory lane. Apple has a long history with Adobe, and although the journey has been filled with ups and downs...
+
+
+
+
+
+
+ AWS Amplify Auth Web UI for iOS | SwiftUI 2.0, Xcode 12
+
+
+ 2020-08-05 00:53:10 +0000
+ VIDEO
+ Kilo Loco goes over how to implement AWS Amplify Auth Web UI into an iOS app from scratch.
+
+We will be using the prebuilt AWS Cognito Auth flow via a web interface to handle the entire flow for signing up, confirming an email address, and...
+
+
+
+
+
+
+ Downloading Gatsby Static Search Index Asynchronously
+
+
+ 2020-08-05 00:00:00 +0000
+ Last year, I rewrote this blog in Gatsby where it had previously used Middleman . I launched the new site with a blog post ; in the…
+
+
+
+
+
+
+ HTTP in Swift, Part 16: Composite Loaders
+
+
+ 2020-08-05 00:00:00 +0000
+ So far we’ve built two different loaders that handle authentication, and it’s conceivable we’d want to build more to support others. Wouldn’t it be nice if we could encapsulate all of the “authentication” logic into a single loader?
+
+
+
+
+
+
+ Menus in SwiftUI
+
+
+ 2020-08-05 00:00:00 +0000
+ This week we got another Xcode Beta that brings menus into SwiftUI world. Menus are going to replace old action sheets that have been here since iOS 8. Action sheets don’t play well with huge screens that we have nowadays. This week we...
+
+
+
+
+
+
+ SKAdNetwork and iOS 14 Privacy Changes
+
+
+ 2020-08-04 21:28:06 +0000
+ What we currently know and preparing for what’s to come.
+
+
+
+
+
+
+ iMac 2020
+
+
+ 2020-08-04 20:42:27 +0000
+ Apple (MacRumors, Hacker News): Apple today announced a major update to its 27-inch iMac. By far the most powerful and capable iMac ever, it features faster Intel processors up to 10 cores, double the memory capacity, next-generation AMD...
+
+
+
+
+
+
+ Swift Package Manager is a security risk
+
+
+ 2020-08-04 19:17:03 +0000
+ Swift Package Manager is a potential security riskLet me start off by saying I absolutely love Swift Package Manager and I think it’s the future of dependency management on iOS, so-much-so I’ve written posts on what we can do with it.But I...
+
+
+
+
+
+
+ Swift Package Manager is a security risk
+
+
+ 2020-08-04 19:17:03 +0000
+ Swift Package Manager is a potential security riskLet me start off by saying I absolutely love Swift Package Manager and I think it’s the future of dependency management on iOS, so-much-so I’ve written posts on what we can do with it.But I...
+
+
+
+
+
+
+ Design for the iPadOS pointer
+
+
+ 2020-08-04 14:21:39 +0000
+ Bring the power of the pointer to your iPad app: We’ll show you how Apple's design team approached designing the iPadOS pointer to complement touch input, and how you can customize and refine pointer interactions in your app to make...
+
+
+
+
+
+
+ How to sort a dictionary by value with Swift
+
+
+ 2020-08-04 13:41:07 +0000
+ Every now and then you might need to sort a dictionary by value, luckily, dictionary comes with a sorted method built in. I am going to use the following dictionary as an example:let dictionary = [
+ "a": "A",
+ "b": "B",
+ "c":...
+
+
+
+
+
+
+ Q&A: What hardware and software do I use to produce podcasts?
+
+
+ 2020-08-04 12:50:00 +0000
+ At first, it might seem like starting a podcast requires a ton of expensive equipment and software, and while it’s certainly true that you can spend a quite enormous amount of money on various kinds of audio gear, I really don’t think...
+
+
+
+
+
+
+ Longest peak in array
+
+
+ 2020-08-04 10:17:50 +0000
+ Write a function that accepts an array of integers and returns the length
+of the longest peak in the array.
+
+
+
+
+
+
+ 42: Hand-Curating Your User Interface with Jeremy Sinon and William White of PodMN
+
+
+ 2020-08-04 10:00:00 +0000
+ Minnesotans love to talk about Minnesota, and nothing proves that more than PodMN’s collection of over 760 Minnesota-focused shows. Jeremy Sinon and William White of PodMN join the show to discuss the custom podcast platform they built to...
+
+
+
+
+
+
+
+
+
+ My 2020 planner setup
+
+
+ 2020-08-04 06:42:00 +0000
+ It's a bit late, but I'm settled into my planner setup for 2020, so I thought I'd share what I've picked this year.
+
+This is my first Jibun Techo-free year since 2017 and I miss it! I picked a Hobonichi Cousin Avec, which has very similar...
+
+
+
+
+
+
+ An intro to Machine Learning in iOS with Swift, and Playgrounds
+
+
+ 2020-08-03 23:31:59 +0000
+ So you heard about machine learning and Apples framework CoreML and wanted to give it a whirl. If your initial thought was that it's too complicated and didn't know where to begin, don't worry, it's not, and I'll walk you through it.
+The...
+
+
+
+
+
+
+ The Dogma of Mocks and Protocols
+
+
+ 2020-08-03 22:00:00 +0000
+ This post is a personal opinionated piece around unit testing and the dogmas of our industry. Be sure to come here with an open mind and respectul toughts. I will also link to some Pointfree content so make sure you check them out.The...
+
+
+
+
+
+
+ nef Playgrounds for iPad 1.1 is now available
+
+
+ 2020-08-03 22:00:00 +0000
+ Just two months ago, we released nef Playgrounds–an iPad application that, together with its corresponding backend, showcases the functional ecosystem that we have been building around Swift and Bow. We have been playing with it a lot,...
+
+
+
+
+
+
+ The Object is the Advantage
+
+
+ 2020-08-03 19:13:51 +0000
+ NeXT marketed their workstations by letting Sun convince people they wanted a workstation, then trying to convince customers (who were already impressed by Sun) that their workstation was better. As part of this, they showed how much...
+
+
+
+
+
+
+ Swift types with @AppStorage and @SceneStorage
+
+
+ 2020-08-03 17:00:00 +0000
+ @AppStorage and @SceneStorage are two SwiftUI property wrappers that have been introduced this year.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Resources/Views/about.md b/Resources/Views/about.md
new file mode 100644
index 0000000..110783b
--- /dev/null
+++ b/Resources/Views/about.md
@@ -0,0 +1,3 @@
+# About
+
+Coming Soon...
diff --git a/Resources/Views/support.md b/Resources/Views/support.md
new file mode 100644
index 0000000..4f92fbe
--- /dev/null
+++ b/Resources/Views/support.md
@@ -0,0 +1,3 @@
+# Support
+
+Coming Soon...
diff --git a/Scripts/script.sh b/Scripts/script.sh
index 6738752..8915708 100644
--- a/Scripts/script.sh
+++ b/Scripts/script.sh
@@ -1,7 +1,7 @@
#!/bin/bash
if [[ $TRAVIS_OS_NAME = 'osx' ]]; then
- swiftformat --lint . && swiftlint
+ swift run swiftformat --lint . && swift run swiftlint
elif [[ $TRAVIS_OS_NAME = 'linux' ]]; then
# What to do in Ubunutu
RELEASE_DOT=$(lsb_release -sr)
diff --git a/Sources/OrchardNestKit/BlogReader.swift b/Sources/OrchardNestKit/BlogReader.swift
deleted file mode 100644
index 19f7dfa..0000000
--- a/Sources/OrchardNestKit/BlogReader.swift
+++ /dev/null
@@ -1,11 +0,0 @@
-import Foundation
-
-public class BlogReader {
- public init() {}
-
- public func sites(fromURL url: URL) throws -> [LanguageContent] {
- let decoder = JSONDecoder()
- let data = try Data(contentsOf: url)
- return try decoder.decode([LanguageContent].self, from: data)
- }
-}
diff --git a/Sources/OrchardNestKit/EntryChannel.swift b/Sources/OrchardNestKit/EntryChannel.swift
new file mode 100644
index 0000000..ee5c204
--- /dev/null
+++ b/Sources/OrchardNestKit/EntryChannel.swift
@@ -0,0 +1,29 @@
+import Foundation
+
+public struct EntryChannel: Codable {
+ public let id: UUID
+ public let title: String
+ public let author: String
+ public let siteURL: URL
+ public let twitterHandle: String?
+ public let imageURL: URL?
+ public let podcastAppleId: Int?
+
+ public init(
+ id: UUID,
+ title: String,
+ siteURL: URL,
+ author: String,
+ twitterHandle: String?,
+ imageURL: URL?,
+ podcastAppleId: Int?
+ ) {
+ self.id = id
+ self.title = title
+ self.siteURL = siteURL
+ self.author = author
+ self.twitterHandle = twitterHandle
+ self.imageURL = imageURL
+ self.podcastAppleId = podcastAppleId
+ }
+}
diff --git a/Sources/OrchardNestKit/EntryItem.swift b/Sources/OrchardNestKit/EntryItem.swift
new file mode 100644
index 0000000..26400c5
--- /dev/null
+++ b/Sources/OrchardNestKit/EntryItem.swift
@@ -0,0 +1,155 @@
+import Foundation
+
+struct IncompleteCategoryType: Error {
+ let type: EntryCategoryType
+}
+
+public enum EntryCategoryType: String, Codable {
+ case companies
+ case design
+ case development
+ case marketing
+ case newsletters
+ case podcasts
+ case updates
+ case youtube
+}
+
+struct EntryCategoryCodable: Codable {
+ let type: EntryCategoryType
+ let value: String?
+}
+
+public enum EntryCategory: Codable {
+ public init(podcastEpisodeAtURL url: URL) {
+ self = .podcasts(url)
+ }
+
+ public init(youtubeVideoWithID id: String) {
+ self = .youtube(id)
+ }
+
+ public init(type: EntryCategoryType) throws {
+ switch type {
+ case .companies: self = .companies
+ case .design: self = .design
+ case .development: self = .development
+ case .marketing: self = .marketing
+ case .newsletters: self = .newsletters
+ case .updates: self = .updates
+ default:
+ throw IncompleteCategoryType(type: type)
+ }
+ }
+
+ public init(from decoder: Decoder) throws {
+ let codable = try EntryCategoryCodable(from: decoder)
+
+ switch codable.type {
+ case .companies: self = .companies
+ case .design: self = .design
+ case .development: self = .development
+ case .marketing: self = .marketing
+ case .newsletters: self = .newsletters
+ case .updates: self = .updates
+ case .podcasts:
+ guard let url = codable.value.flatMap(URL.init(string:)) else {
+ throw DecodingError.valueNotFound(URL.self, DecodingError.Context(codingPath: [], debugDescription: ""))
+ }
+ self = .podcasts(url)
+ case .youtube:
+ guard let id = codable.value else {
+ throw DecodingError.valueNotFound(URL.self, DecodingError.Context(codingPath: [], debugDescription: ""))
+ }
+ self = .youtube(id)
+ }
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ let codable = EntryCategoryCodable(type: type, value: value)
+ try codable.encode(to: encoder)
+ }
+
+ case companies
+ case design
+ case development
+ case marketing
+ case newsletters
+ case podcasts(URL)
+ case updates
+ case youtube(String)
+
+ public var type: EntryCategoryType {
+ switch self {
+ case .companies: return .companies
+ case .design: return .design
+ case .development: return .development
+ case .marketing: return .marketing
+ case .newsletters: return .newsletters
+ case .podcasts: return .podcasts
+ case .updates: return .updates
+ case .youtube: return .youtube
+ }
+ }
+
+ var value: String? {
+ switch self {
+ case let .podcasts(url): return url.absoluteString
+ case let .youtube(id): return id
+ default: return nil
+ }
+ }
+}
+
+public struct EntryItem: Codable {
+ public let id: UUID
+ public let channel: EntryChannel
+ public let feedId: String
+ public let title: String
+ public let summary: String
+ public let url: URL
+ public let imageURL: URL?
+ public let publishedAt: Date
+ public let category: EntryCategory
+
+ public init(id: UUID,
+ channel: EntryChannel,
+ category: EntryCategory,
+ feedId: String,
+ title: String,
+ summary: String,
+ url: URL,
+ imageURL: URL?,
+ publishedAt: Date) {
+ self.id = id
+ self.channel = channel
+ self.feedId = feedId
+ self.title = title
+ self.summary = summary
+ self.url = url
+ self.imageURL = imageURL
+ self.category = category
+ self.publishedAt = publishedAt
+ }
+}
+
+public extension EntryItem {
+ var podcastEpisodeURL: URL? {
+ if case let .podcasts(url) = category {
+ return url
+ }
+ return nil
+ }
+
+ var youtubeID: String? {
+ if case let .youtube(id) = category {
+ return id
+ }
+ return nil
+ }
+
+ var twitterShareLink: String {
+ let text = title + (channel.twitterHandle.map { " from @\($0)" } ?? "")
+ return "https://twitter.com/intent/tweet?text=\(text)&via=orchardnest&url=\(url)"
+ }
+}
diff --git a/Sources/OrchardNestKit/Channel.swift b/Sources/OrchardNestKit/FeedChannel.swift
similarity index 77%
rename from Sources/OrchardNestKit/Channel.swift
rename to Sources/OrchardNestKit/FeedChannel.swift
index dfb6ddf..2145f4c 100644
--- a/Sources/OrchardNestKit/Channel.swift
+++ b/Sources/OrchardNestKit/FeedChannel.swift
@@ -1,7 +1,16 @@
import FeedKit
import Foundation
-public struct Channel: Codable {
+extension URL {
+ func ensureAbsolute(_ baseURL: URL) -> URL {
+ guard host == nil else {
+ return self
+ }
+ return URL(string: relativeString, relativeTo: baseURL) ?? self
+ }
+}
+
+public struct FeedChannel: Codable {
static let youtubeImgBaseURL = URL(string: "https://img.youtube.com/vi/")!
public static func imageURL(fromYoutubeId ytId: String) -> URL {
return youtubeImgBaseURL.appendingPathComponent(ytId).appendingPathComponent("hqdefault.jpg")
@@ -18,13 +27,14 @@ public struct Channel: Codable {
public let ytId: String?
public let language: String
public let category: String
- public let items: [Item]
+ public let items: [FeedItem]
public let itemCount: Int?
// swiftlint:disable:next function_body_length
- public init(language: String, category: String, site: Site) throws {
- let parser = FeedParser(URL: site.feed_url)
+ public init(language: String, category: String, site: Site, data: Data) throws {
+ let parser = FeedParser(data: data)
let feed = try parser.parse().get()
+
switch feed {
case let .json(json):
title = json.title ?? site.title
@@ -41,19 +51,21 @@ public struct Channel: Codable {
self.category = category
itemCount = json.items?.count
- items = json.items?.compactMap { (item) -> Item? in
+ items = json.items?.compactMap { (item) -> FeedItem? in
let siteUrl: URL = site.site_url
guard let title = item.title,
let summary = item.summary,
let url = item.externalUrl.flatMap(URL.init(string:)) ?? item.url.flatMap(URL.init(string:)),
- let id = item.id ?? item.url ?? item.externalUrl else {
+ let id = item.id ?? item.url ?? item.externalUrl,
+ let published = item.datePublished ?? item.dateModified
+ else {
return nil
}
let content = item.contentHtml ?? item.contentText
let image = item.image.flatMap(URL.init(string:)) ?? item.bannerImage.flatMap(URL.init(string:))
- let published = item.datePublished ?? item.dateModified ?? Date()
- return Item(
+
+ return FeedItem(
siteUrl: siteUrl,
id: id,
title: title,
@@ -65,7 +77,7 @@ public struct Channel: Codable {
audio: nil,
published: published
)
- } ?? [Item]()
+ } ?? [FeedItem]()
case let .rss(rss):
title = rss.title ?? site.title
@@ -74,7 +86,7 @@ public struct Channel: Codable {
siteUrl = rss.link.flatMap(URL.init(string:)) ?? site.site_url
feedUrl = site.feed_url
twitterHandle = site.twitter_url?.lastPathComponent
- image = rss.image?.url.flatMap(URL.init(string:))
+ image = rss.image?.url.flatMap(URL.init(string:)) ?? rss.iTunes?.iTunesImage?.attributes?.href.flatMap(URL.init(string:))
// self.image = atom.image
updated = rss.pubDate ?? Date()
self.language = language
@@ -82,7 +94,7 @@ public struct Channel: Codable {
ytId = nil
itemCount = rss.items?.count
- items = rss.items?.compactMap { (item) -> Item? in
+ items = rss.items?.compactMap { (item) -> FeedItem? in
let siteUrl: URL = site.site_url
guard let title = item.title,
@@ -90,9 +102,12 @@ public struct Channel: Codable {
item.content?.contentEncoded ??
item.media?.mediaDescription?.value,
let id = item.guid?.value ?? item.link,
- let url = item.link.flatMap(URL.init(string:)) else {
+ let itemUrl = item.link.flatMap(URL.init(string:)),
+ let published = item.pubDate ?? item.dublinCore?.dcDate
+ else {
return nil
}
+ let url = itemUrl.ensureAbsolute(siteUrl)
let enclosure = item.enclosure.flatMap(Enclosure.init)
let content = item.content?.contentEncoded
let image = item.iTunes?.iTunesImage?.attributes?.href.flatMap(URL.init(string:)) ??
@@ -102,8 +117,8 @@ public struct Channel: Codable {
}.first
// let ytId: String
// let itId = item.media.
- let published = item.pubDate ?? Date()
- return Item(
+
+ return FeedItem(
siteUrl: siteUrl,
id: id,
title: title,
@@ -115,7 +130,7 @@ public struct Channel: Codable {
audio: enclosure?.audioURL,
published: published
)
- } ?? [Item]()
+ } ?? [FeedItem]()
case let .atom(atom):
title = atom.title ?? site.title
@@ -140,7 +155,7 @@ public struct Channel: Codable {
URL(string: $0, relativeTo: site.feed_url)
} ?? ytId.map(Self.imageURL)
self.ytId = ytId
- items = atom.entries?.compactMap { (entry) -> Item? in
+ items = atom.entries?.compactMap { (entry) -> FeedItem? in
let siteUrl: URL = site.site_url
let media = entry.links?.compactMap(Enclosure.init(element:))
guard let title = entry.title else {
@@ -148,22 +163,28 @@ public struct Channel: Codable {
}
guard let summary = entry.summary?.value ??
entry.content?.value ??
- entry.media?.mediaGroup?.mediaDescription?.value else {
+ entry.media?.mediaGroup?.mediaDescription?.value
+ else {
return nil
}
- guard let url: URL = entry.links?.first?.attributes?.href.flatMap(URL.init(string:)) else {
+ guard let entryUrl: URL = entry.links?.first?.attributes?.href.flatMap(URL.init(string:)) else {
return nil
}
guard let id = entry.id else {
return nil
}
+
+ guard let published = entry.published else {
+ return nil
+ }
let ytId: String?
if id.starts(with: "yt:video:") {
ytId = id.components(separatedBy: ":").last
} else {
ytId = nil
}
- return Item(
+ let url = entryUrl.ensureAbsolute(siteUrl)
+ return FeedItem(
siteUrl: siteUrl,
id: id,
title: title,
@@ -173,9 +194,9 @@ public struct Channel: Codable {
image: media?.compactMap { $0.imageURL }.first,
ytId: ytId,
audio: media?.compactMap { $0.audioURL }.first,
- published: entry.published ?? Date()
+ published: published
)
- } ?? [Item]()
+ } ?? [FeedItem]()
}
}
}
diff --git a/Sources/OrchardNestKit/Item.swift b/Sources/OrchardNestKit/FeedItem.swift
similarity index 89%
rename from Sources/OrchardNestKit/Item.swift
rename to Sources/OrchardNestKit/FeedItem.swift
index f0de376..0864bd2 100644
--- a/Sources/OrchardNestKit/Item.swift
+++ b/Sources/OrchardNestKit/FeedItem.swift
@@ -1,6 +1,6 @@
import Foundation
-public struct Item: Codable {
+public struct FeedItem: Codable {
public let siteUrl: URL
public let id: String
public let title: String
diff --git a/Sources/OrchardNestServer/Configurator.swift b/Sources/OrchardNestServer/Configurator.swift
new file mode 100644
index 0000000..1623fbc
--- /dev/null
+++ b/Sources/OrchardNestServer/Configurator.swift
@@ -0,0 +1,152 @@
+import Fluent
+import FluentPostgresDriver
+import Ink
+import OrchardNestKit
+import Plot
+import QueuesFluentDriver
+import Vapor
+extension Date {
+ func get(_ type: Calendar.Component) -> Int {
+ let calendar = Calendar.current
+ return calendar.component(type, from: self)
+ }
+}
+
+extension HTML: ResponseEncodable {
+ public func encodeResponse(for request: Request) -> EventLoopFuture {
+ var headers = HTTPHeaders()
+ headers.add(name: .contentType, value: "text/html")
+ return request.eventLoop.makeSucceededFuture(.init(
+ status: .ok, headers: headers, body: .init(string: render())
+ ))
+ }
+}
+
+struct OrganizedSite {
+ let languageCode: String
+ let categorySlug: String
+ let site: Site
+}
+
+//
+public final class Configurator: ConfiguratorProtocol {
+ public static let shared: ConfiguratorProtocol = Configurator()
+
+ //
+ ///// Called before your application initializes.
+ public func configure(_ app: Application) throws {
+ // Register providers first
+ // try services.register(FluentPostgreSQLProvider())
+ // try services.register(AuthenticationProvider())
+
+ // services.register(DirectoryIndexMiddleware.self)
+
+ // Register middleware
+ // var middlewares = MiddlewareConfig() // Create _empty_ middleware config
+ // middlewares.use(SessionsMiddleware.self) // Enables sessions.
+ // let rootPath = Environment.get("ROOT_PATH") ?? app.directory.publicDirectory
+
+// app.webSockets = WebSocketRepository()
+//
+// app.middleware.use(DirectoryIndexMiddleware(publicDirectory: rootPath))
+
+ app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
+
+// // Configure Leaf
+// app.views.use(.leaf)
+// app.leaf.cache.isEnabled = app.environment.isRelease
+// app.middleware.use(ErrorMiddleware.default(environment: app.environment))
+ // middlewares.use(ErrorMiddleware.self) // Catches errors and converts to HTTP response
+ // services.register(middlewares)
+
+ // Configure a SQLite database
+ let postgreSQLConfig: PostgresConfiguration
+
+ if let url = Environment.get("DATABASE_URL") {
+ postgreSQLConfig = PostgresConfiguration(url: url)!
+ } else {
+ postgreSQLConfig = PostgresConfiguration(hostname: "localhost", username: "orchardnest")
+ }
+
+ app.databases.use(.postgres(configuration: postgreSQLConfig, maxConnectionsPerEventLoop: 8, connectionPoolTimeout: .seconds(60)), as: .psql)
+ app.migrations.add([
+ CategoryMigration(),
+ LanguageMigration(),
+ CategoryTitleMigration(),
+ ChannelMigration(),
+ EntryMigration(),
+ PodcastEpisodeMigration(),
+ YouTubeChannelMigration(),
+ YouTubeVideoMigration(),
+ PodcastChannelMigration(),
+ ChannelStatusMigration(),
+ LatestEntriesMigration(),
+ JobModelMigrate(schema: "queue_jobs")
+ ])
+
+ app.queues.configuration.refreshInterval = .seconds(25)
+ app.queues.use(.fluent())
+// app.databases.middleware.use(UserEmailerMiddleware(app: app))
+//
+// app.migrations.add(CreateDevice())
+// app.migrations.add(CreateAppleUser())
+// app.migrations.add(CreateDeviceWorkout())
+// app.migrations.add(ActivateWorkout())
+ // let wss = NIOWebSocketServer.default()
+
+// app.webSocket("api", "v1", "workouts", ":id", "listen") { req, websocket in
+// guard let idData = try? Base32CrockfordEncoding.encoding.decode(base32Encoded: req.parameters.get("id")!) else {
+// return
+// }
+// let workoutID = UUID(data: idData)
+//
+// _ = Workout.find(workoutID, on: req.db).unwrap(or: Abort(HTTPResponseStatus.notFound)).flatMapThrowing { workout in
+// let workoutId = try workout.requireID()
+// app.webSockets.save(websocket, withID: workoutId)
+// }
+// }
+
+ app.queues.add(RefreshJob())
+ app.queues.schedule(RefreshJob()).daily().at(.midnight)
+ app.queues.schedule(RefreshJob()).daily().at(7, 30)
+ app.queues.schedule(RefreshJob()).daily().at(19, 30)
+ #if DEBUG
+ if !app.environment.isRelease {
+ let minute = Date().get(.minute)
+ [0, 30].map { ($0 + minute + 5).remainderReportingOverflow(dividingBy: 60).partialValue }.forEach { minute in
+ app.queues.schedule(RefreshJob()).hourly().at(.init(integerLiteral: minute))
+ }
+ }
+ #endif
+ try app.queues.startInProcessJobs(on: .default)
+ app.commands.use(RefreshCommand(help: "Imports data into the database"), as: "refresh")
+
+ try app.autoMigrate().wait()
+ // services.register(wss, as: WebSocketServer.self)
+
+ let api = app.grouped("api", "v1")
+
+ let markdownDirectory = app.directory.viewsDirectory
+ let parser = MarkdownParser()
+
+ let textPairs = FileManager.default.enumerator(atPath: markdownDirectory)?.compactMap { $0 as? String }.map { path in
+ URL(fileURLWithPath: app.directory.viewsDirectory + path)
+ }.compactMap { url in
+ (try? String(contentsOf: url)).map { (url.deletingPathExtension().lastPathComponent, $0) }
+ }
+
+ let pages = textPairs.map(Dictionary.init(uniqueKeysWithValues:))?.mapValues(
+ parser.parse
+ )
+
+ try app.register(collection: HTMLController(views: pages))
+ try api.grouped("entires").register(collection: EntryController())
+
+ app.post("jobs") { req in
+ req.queue.dispatch(
+ RefreshJob.self,
+ RefreshConfiguration()
+ ).map { HTTPStatus.created }
+ }
+ }
+}
diff --git a/Sources/OrchardNestServer/ConfiguratorProtocol.swift b/Sources/OrchardNestServer/ConfiguratorProtocol.swift
new file mode 100644
index 0000000..dbd67e4
--- /dev/null
+++ b/Sources/OrchardNestServer/ConfiguratorProtocol.swift
@@ -0,0 +1,5 @@
+import Vapor
+
+public protocol ConfiguratorProtocol {
+ func configure(_ app: Application) throws
+}
diff --git a/Sources/OrchardNestServer/Controllers/DB/Migration/CategoryMigration.swift b/Sources/OrchardNestServer/Controllers/DB/Migration/CategoryMigration.swift
new file mode 100644
index 0000000..d94196f
--- /dev/null
+++ b/Sources/OrchardNestServer/Controllers/DB/Migration/CategoryMigration.swift
@@ -0,0 +1,14 @@
+import Fluent
+import Vapor
+
+struct CategoryMigration: Migration {
+ func prepare(on database: Database) -> EventLoopFuture {
+ database.schema(Category.schema)
+ .field("slug", .string, .identifier(auto: false))
+ .create()
+ }
+
+ func revert(on database: Database) -> EventLoopFuture {
+ database.schema(Category.schema).delete()
+ }
+}
diff --git a/Sources/OrchardNestServer/Controllers/DB/Migration/CategoryTitleMigration.swift b/Sources/OrchardNestServer/Controllers/DB/Migration/CategoryTitleMigration.swift
new file mode 100644
index 0000000..930e783
--- /dev/null
+++ b/Sources/OrchardNestServer/Controllers/DB/Migration/CategoryTitleMigration.swift
@@ -0,0 +1,18 @@
+import Fluent
+import Vapor
+
+struct CategoryTitleMigration: Migration {
+ func prepare(on database: Database) -> EventLoopFuture {
+ database.schema(CategoryTitle.schema)
+ .id()
+ .field("code", .string, .references(Language.schema, "code"))
+ .field("slug", .string, .references(Category.schema, "slug"))
+ .field("title", .string, .required)
+ .unique(on: "code", "slug")
+ .create()
+ }
+
+ func revert(on database: Database) -> EventLoopFuture {
+ database.schema(CategoryTitle.schema).delete()
+ }
+}
diff --git a/Sources/OrchardNestServer/Controllers/DB/Migration/ChannelMigration.swift b/Sources/OrchardNestServer/Controllers/DB/Migration/ChannelMigration.swift
new file mode 100644
index 0000000..248d9bf
--- /dev/null
+++ b/Sources/OrchardNestServer/Controllers/DB/Migration/ChannelMigration.swift
@@ -0,0 +1,27 @@
+import Fluent
+import Vapor
+
+struct ChannelMigration: Migration {
+ func prepare(on database: Database) -> EventLoopFuture {
+ database.schema(Channel.schema)
+ .id()
+ .field("language_code", .string, .references(Language.schema, "code"))
+ .field("category_slug", .string, .references(Category.schema, "slug"))
+ .field("title", .string, .required)
+ .field("subtitle", .string)
+ .field("author", .string, .required)
+ .field("site_url", .string, .required)
+ .field("feed_url", .string, .required)
+ .field("twitter_handle", .string)
+ .field("image", .string)
+ .field("published_at", .datetime, .required)
+ .field("created_at", .datetime, .required)
+ .field("updated_at", .datetime, .required)
+ .unique(on: "feed_url")
+ .create()
+ }
+
+ func revert(on database: Database) -> EventLoopFuture {
+ database.schema(Channel.schema).delete()
+ }
+}
diff --git a/Sources/OrchardNestServer/Controllers/DB/Migration/ChannelStatusMigration.swift b/Sources/OrchardNestServer/Controllers/DB/Migration/ChannelStatusMigration.swift
new file mode 100644
index 0000000..7215659
--- /dev/null
+++ b/Sources/OrchardNestServer/Controllers/DB/Migration/ChannelStatusMigration.swift
@@ -0,0 +1,22 @@
+import Fluent
+import Vapor
+
+struct ChannelStatusMigration: Migration {
+ func prepare(on database: Database) -> EventLoopFuture {
+ var channelStatusType = database.enum("channel_status_type")
+ for type in ChannelStatusType.allCases {
+ channelStatusType = channelStatusType.case(type.rawValue)
+ }
+ return channelStatusType.create().flatMap { channelStatusType in
+
+ database.schema(ChannelStatus.schema)
+ .field("feed_url", .string, .identifier(auto: false))
+ .field("status", channelStatusType, .required)
+ .create()
+ }
+ }
+
+ func revert(on database: Database) -> EventLoopFuture {
+ database.schema(ChannelStatus.schema).delete()
+ }
+}
diff --git a/Sources/OrchardNestServer/Controllers/DB/Migration/EntryMigration.swift b/Sources/OrchardNestServer/Controllers/DB/Migration/EntryMigration.swift
new file mode 100644
index 0000000..3788219
--- /dev/null
+++ b/Sources/OrchardNestServer/Controllers/DB/Migration/EntryMigration.swift
@@ -0,0 +1,24 @@
+import Fluent
+import Vapor
+
+struct EntryMigration: Migration {
+ func prepare(on database: Database) -> EventLoopFuture {
+ database.schema(Entry.schema)
+ .id()
+ .field("channel_id", .uuid, .required)
+ .field("feed_id", .string, .required)
+ .field("title", .string, .required)
+ .field("summary", .string, .required)
+ .field("content", .string)
+ .field("url", .string, .required)
+ .field("image", .string)
+ .field("published_at", .datetime, .required)
+ .field("created_at", .datetime, .required)
+ .field("updated_at", .datetime, .required)
+ .create()
+ }
+
+ func revert(on database: Database) -> EventLoopFuture {
+ database.schema(Entry.schema).delete()
+ }
+}
diff --git a/Sources/OrchardNestServer/Controllers/DB/Migration/LanguageMigration.swift b/Sources/OrchardNestServer/Controllers/DB/Migration/LanguageMigration.swift
new file mode 100644
index 0000000..5c462e3
--- /dev/null
+++ b/Sources/OrchardNestServer/Controllers/DB/Migration/LanguageMigration.swift
@@ -0,0 +1,15 @@
+import Fluent
+import Vapor
+
+struct LanguageMigration: Migration {
+ func prepare(on database: Database) -> EventLoopFuture {
+ database.schema(Language.schema)
+ .field("code", .string, .identifier(auto: false))
+ .field("title", .string, .required)
+ .create()
+ }
+
+ func revert(on database: Database) -> EventLoopFuture {
+ database.schema(Language.schema).delete()
+ }
+}
diff --git a/Sources/OrchardNestServer/Controllers/DB/Migration/LatestEntriesMigration.swift b/Sources/OrchardNestServer/Controllers/DB/Migration/LatestEntriesMigration.swift
new file mode 100644
index 0000000..3852f7a
--- /dev/null
+++ b/Sources/OrchardNestServer/Controllers/DB/Migration/LatestEntriesMigration.swift
@@ -0,0 +1,43 @@
+import FluentSQL
+import Vapor
+
+struct LatestEntriesMigration: Migration {
+ func prepare(on database: Database) -> EventLoopFuture {
+ guard let sql = database as? SQLDatabase else {
+ return database.eventLoop.makeFailedFuture(InvalidDatabaseError())
+ }
+
+ return sql.raw("""
+ -- DDL generated by Postico 1.5.14
+ -- Not all database features are supported. Do not use for backup.
+
+ -- Table Definition ----------------------------------------------
+
+ CREATE VIEW latest_entries AS SELECT latest.id,
+ latest.channel_id
+ FROM ( SELECT DISTINCT ON (entries.channel_id) entries.id,
+ entries.channel_id,
+ entries.feed_id,
+ entries.title,
+ entries.summary,
+ entries.content,
+ entries.url,
+ entries.image,
+ entries.published_at,
+ entries.created_at,
+ entries.updated_at
+ FROM entries
+ ORDER BY entries.channel_id, entries.published_at DESC) latest
+ ORDER BY latest.published_at DESC;
+
+ """
+ ).run()
+ }
+
+ func revert(on database: Database) -> EventLoopFuture {
+ guard let sql = database as? SQLDatabase else {
+ return database.eventLoop.makeFailedFuture(InvalidDatabaseError())
+ }
+ return sql.raw("drop view if exists latest_entries").run()
+ }
+}
diff --git a/Sources/OrchardNestServer/Controllers/DB/Migration/PodcastChannelMigration.swift b/Sources/OrchardNestServer/Controllers/DB/Migration/PodcastChannelMigration.swift
new file mode 100644
index 0000000..9c2e3f2
--- /dev/null
+++ b/Sources/OrchardNestServer/Controllers/DB/Migration/PodcastChannelMigration.swift
@@ -0,0 +1,16 @@
+import Fluent
+import Vapor
+
+struct PodcastChannelMigration: Migration {
+ func prepare(on database: Database) -> EventLoopFuture {
+ database.schema(PodcastChannel.schema)
+ .field("channel_id", .uuid, .identifier(auto: false), .references(Channel.schema, .id))
+ .field("apple_id", .int, .required)
+ .unique(on: "apple_id")
+ .create()
+ }
+
+ func revert(on database: Database) -> EventLoopFuture {
+ database.schema(PodcastChannel.schema).delete()
+ }
+}
diff --git a/Sources/OrchardNestServer/Controllers/DB/Migration/PodcastEpisodeMigration.swift b/Sources/OrchardNestServer/Controllers/DB/Migration/PodcastEpisodeMigration.swift
new file mode 100644
index 0000000..ec4aa2e
--- /dev/null
+++ b/Sources/OrchardNestServer/Controllers/DB/Migration/PodcastEpisodeMigration.swift
@@ -0,0 +1,15 @@
+import Fluent
+import Vapor
+
+struct PodcastEpisodeMigration: Migration {
+ func prepare(on database: Database) -> EventLoopFuture {
+ database.schema(PodcastEpisode.schema)
+ .field("entry_id", .uuid, .identifier(auto: false), .references(Entry.schema, .id))
+ .field("audio", .string, .required)
+ .create()
+ }
+
+ func revert(on database: Database) -> EventLoopFuture {
+ database.schema(PodcastEpisode.schema).delete()
+ }
+}
diff --git a/Sources/OrchardNestServer/Controllers/DB/Migration/YouTubeChannelMigration.swift b/Sources/OrchardNestServer/Controllers/DB/Migration/YouTubeChannelMigration.swift
new file mode 100644
index 0000000..5e7eda2
--- /dev/null
+++ b/Sources/OrchardNestServer/Controllers/DB/Migration/YouTubeChannelMigration.swift
@@ -0,0 +1,16 @@
+import Fluent
+import Vapor
+
+struct YouTubeChannelMigration: Migration {
+ func prepare(on database: Database) -> EventLoopFuture {
+ database.schema(YouTubeChannel.schema)
+ .field("channel_id", .uuid, .identifier(auto: false), .references(Channel.schema, .id))
+ .field("youtube_id", .string, .required)
+ .unique(on: "youtube_id")
+ .create()
+ }
+
+ func revert(on database: Database) -> EventLoopFuture {
+ database.schema(YouTubeChannel.schema).delete()
+ }
+}
diff --git a/Sources/OrchardNestServer/Controllers/DB/Migration/YouTubeVideoMigration.swift b/Sources/OrchardNestServer/Controllers/DB/Migration/YouTubeVideoMigration.swift
new file mode 100644
index 0000000..1be46c0
--- /dev/null
+++ b/Sources/OrchardNestServer/Controllers/DB/Migration/YouTubeVideoMigration.swift
@@ -0,0 +1,16 @@
+import Fluent
+import Vapor
+
+struct YouTubeVideoMigration: Migration {
+ func prepare(on database: Database) -> EventLoopFuture {
+ database.schema(YoutubeVideo.schema)
+ .field("entry_id", .uuid, .identifier(auto: false), .references(Entry.schema, .id))
+ .field("youtube_id", .string, .required)
+ .unique(on: "youtube_id")
+ .create()
+ }
+
+ func revert(on database: Database) -> EventLoopFuture {
+ database.schema(YoutubeVideo.schema).delete()
+ }
+}
diff --git a/Sources/OrchardNestServer/Controllers/Routing/EntryController.swift b/Sources/OrchardNestServer/Controllers/Routing/EntryController.swift
new file mode 100644
index 0000000..a1e6a14
--- /dev/null
+++ b/Sources/OrchardNestServer/Controllers/Routing/EntryController.swift
@@ -0,0 +1,80 @@
+import Fluent
+import OrchardNestKit
+import Vapor
+
+struct InvalidURLFormat: Error {}
+
+extension String {
+ func asURL() throws -> URL {
+ guard let url = URL(string: self) else {
+ throw InvalidURLFormat()
+ }
+ return url
+ }
+}
+
+extension Entry {
+ func category() throws -> EntryCategory {
+ guard let category = EntryCategoryType(rawValue: channel.$category.id) else {
+ return .development
+ }
+
+ if let url = podcastEpisode.flatMap({ URL(string: $0.audioURL) }) {
+ return .podcasts(url)
+ } else if let youtubeID = youtubeVideo?.youtubeId {
+ return .youtube(youtubeID)
+ } else {
+ return try EntryCategory(type: category)
+ }
+ }
+}
+
+extension EntryChannel {
+ init(channel: Channel) throws {
+ try self.init(
+ id: channel.requireID(),
+ title: channel.title,
+ siteURL: channel.siteUrl.asURL(),
+ author: channel.author,
+ twitterHandle: channel.twitterHandle,
+ imageURL: channel.imageURL?.asURL(),
+ podcastAppleId: channel.$podcasts.value?.first?.appleId
+ )
+ }
+}
+
+extension EntryItem {
+ init(entry: Entry) throws {
+ try self.init(
+ id: entry.requireID(),
+ channel: EntryChannel(channel: entry.channel),
+ category: entry.category(),
+ feedId: entry.feedId,
+ title: entry.title,
+ summary: entry.summary,
+ url: entry.url.asURL(),
+ imageURL: entry.imageURL?.asURL(),
+ publishedAt: entry.publishedAt
+ )
+ }
+}
+
+struct EntryController {
+ func list(req: Request) -> EventLoopFuture> {
+ return Entry.query(on: req.db)
+ .sort(\.$publishedAt, .descending)
+ .with(\.$channel)
+ .paginate(for: req)
+ .flatMapThrowing { (page: Page) -> Page in
+ try page.map { (entry: Entry) -> EntryItem in
+ try EntryItem(entry: entry)
+ }
+ }
+ }
+}
+
+extension EntryController: RouteCollection {
+ func boot(routes: RoutesBuilder) throws {
+ routes.get("", use: list)
+ }
+}
diff --git a/Sources/OrchardNestServer/Controllers/Routing/HTMLController.swift b/Sources/OrchardNestServer/Controllers/Routing/HTMLController.swift
new file mode 100644
index 0000000..def5389
--- /dev/null
+++ b/Sources/OrchardNestServer/Controllers/Routing/HTMLController.swift
@@ -0,0 +1,429 @@
+import Fluent
+import FluentSQL
+import Ink
+import OrchardNestKit
+import Plot
+import Vapor
+
+struct InvalidDatabaseError: Error {}
+
+extension Node where Context == HTML.BodyContext {
+ static func playerForPodcast(withAppleId appleId: Int) -> Self {
+ .ul(
+ .class("podcast-players"),
+ .li(
+ .a(
+ .href("https://podcasts.apple.com/podcast/id\(appleId)"),
+ .img(
+ .src("/images/podcast-players/apple/icon.svg")
+ ),
+ .div(
+ .div(
+ .text("Listen on")
+ ),
+ .div(
+ .class("name"),
+ .text("Apple Podcasts")
+ )
+ )
+ )
+ ),
+ .li(
+ .a(
+ .href("https://overcast.fm/itunes\(appleId)"),
+ .img(
+ .src("/images/podcast-players/overcast/icon.svg")
+ ),
+ .div(
+ .div(
+ .text("Listen on")
+ ),
+ .div(
+ .class("name"),
+ .text("Overcast")
+ )
+ )
+ )
+ ),
+ .li(
+ .a(
+ .href("https://castro.fm/itunes/\(appleId)"),
+ .img(
+ .src("/images/podcast-players/castro/icon.svg")
+ ),
+ .div(
+ .div(
+ .text("Listen on")
+ ),
+ .div(
+ .class("name"),
+ .text("Castro")
+ )
+ )
+ )
+ ),
+ .li(
+ .a(
+ .href("https://podcasts.apple.com/podcast/id\(appleId)"),
+ .img(
+ .src("/images/podcast-players/pocketcasts/icon.svg")
+ ),
+ .div(
+ .div(
+ .text("Listen on")
+ ),
+ .div(
+ .class("name"),
+ .text("Pocket Casts")
+ )
+ )
+ )
+ )
+ )
+ }
+}
+
+extension Node where Context == HTML.BodyContext {
+ static func filters() -> Self {
+ .nav(
+ .class("posts-filter clearfix row"),
+ .ul(
+ .class("column"),
+ .li(.a(.class("button"), .href("/"), .i(.class("el el-calendar")), .text(" Latest"))),
+ .li(.a(.class("button"), .href("/category/development"), .i(.class("el el-cogs")), .text(" Development"))),
+ .li(.a(.class("button"), .href("/category/marketing"), .i(.class("el el-bullhorn")), .text(" Marketing"))),
+ .li(.a(.class("button"), .href("/category/design"), .i(.class("el el-brush")), .text(" Design"))),
+ .li(.a(.class("button"), .href("/category/podcasts"), .i(.class("el el-podcast")), .text(" Podcasts"))),
+ .li(.a(.class("button"), .href("/category/youtube"), .i(.class("el el-video")), .text(" YouTube"))),
+ .li(.a(.class("button"), .href("/category/newsletters"), .i(.class("el el-envelope")), .text(" Newsletters")))
+ )
+ )
+ }
+}
+
+extension Node where Context == HTML.BodyContext {
+ static func header() -> Self {
+ .header(
+ .class("container"),
+ .nav(
+ .class("row"),
+ .ul(
+ .class("column"),
+ .li(.a(.href("/"), .i(.class("el el-home")), .text(" Home"))),
+ .li(.a(.href("/about"), .i(.class("el el-info-circle")), .text(" About"))),
+ .li(.a(.href("/support"), .i(.class("el el-question-sign")), .text(" Support")))
+ ),
+ .ul(.class("float-right column"),
+ .li(.a(.href("https://github.com/brightdigit/OrchardNest"), .i(.class("el el-github")), .text(" GitHub"))),
+ .li(.a(.href("https://twitter.com/OrchardNest"), .i(.class("el el-twitter")), .text(" Twitter"))))
+ ),
+ .div(
+ .class("row"),
+ .h1(
+ .class("column"),
+ .img(
+ .class("logo"),
+ .src("/images/logo.svg")
+ ),
+ .text(" OrchardNest")
+ )
+ ),
+ div(
+ .class("row"),
+ .p(
+ .class("tagline column"),
+ .text("Swift Articles and News")
+ )
+ )
+ )
+ }
+}
+
+extension Node where Context == HTML.DocumentContext {
+ static func head(withSubtitle subtitle: String) -> Self {
+ return
+ .head(
+ .title("OrchardNest - \(subtitle)"),
+ .meta(.charset(.utf8)),
+ .raw("""
+
+
+
+ """),
+ .link(.rel(.appleTouchIcon), .sizes("180x180"), .href("/apple-touch-icon.png")),
+ .link(.rel(.appleTouchIcon), .type("image/png"), .sizes("32x32"), .href("/favicon-32x32.png")),
+ .link(.rel(.appleTouchIcon), .type("image/png"), .sizes("16x16"), .href("/favicon-16x16.png")),
+ .link(.rel(.manifest), .href("/site.webmanifest")),
+ .link(.rel(.maskIcon), .href("/safari-pinned-tab.svg"), .color("#5bbad5")),
+ .meta(.name("msapplication-TileColor"), .content("#2b5797")),
+ .meta(.name("theme-color"), .content("#ffffff")),
+ .link(.rel(.stylesheet), .href("/styles/elusive-icons/css/elusive-icons.min.css")),
+
+ .link(.rel(.stylesheet), .href("/styles/normalize.css")),
+
+ .link(.rel(.stylesheet), .href("/styles/milligram.css")),
+ .link(.rel(.stylesheet), .href("/styles/style.css")),
+ .link(.rel(.stylesheet), .href("https://fonts.googleapis.com/css2?family=Catamaran:wght@100;400;800&display=swap"))
+ )
+ }
+}
+
+extension Node where Context == HTML.ListContext {
+ static func li(forEntryItem item: EntryItem) -> Self {
+ return
+ .li(
+ .class("blog-post"),
+
+ .a(
+ .href(item.url),
+ .class("title"),
+ .h3(
+ .i(.class("el el-\(item.category.elClass)")),
+
+ .text(item.title)
+ )
+ ),
+ .div(
+ .class("publishedAt"),
+ .text(item.publishedAt.description)
+ ),
+ .unwrap(item.youtubeID) {
+ .iframe(
+ .src("https://www.youtube.com/embed/" + $0),
+ .allow("accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"),
+ .allowfullscreen(true)
+ )
+ },
+ .div(
+ .class("summary"),
+ .text(item.summary.plainTextShort)
+ ),
+ .unwrap(item.podcastEpisodeURL) {
+ .audio(
+ .controls(true),
+ .attribute(named: "preload", value: "metadata"),
+ .source(
+ .src($0)
+ )
+ )
+ },
+ .unwrap(item.channel.podcastAppleId) {
+ .playerForPodcast(withAppleId: $0)
+ },
+ .div(
+ .class("author"),
+ .text("By "),
+ .text(item.channel.author),
+ .text(" at "),
+ .a(
+ .href("/channels/" + item.channel.id.uuidString),
+ .text(item.channel.siteURL.host ?? item.channel.title)
+ ),
+ .unwrap(item.channel.twitterHandle) {
+ .a(
+ .href("https://twitter.com/\($0)"),
+ .class("button twitter-handle"),
+ .i(.class("el el-twitter")),
+ .text(" @\($0)")
+ )
+ }
+ ),
+ .div(
+ .class("social-share clearfix"),
+ .text("Share"),
+ .ul(
+ .li(
+ .a(
+ .class("button"),
+ .href(item.twitterShareLink),
+ .i(.class("el el-twitter")),
+ .text(" Tweet")
+ )
+ )
+ )
+ )
+ )
+ }
+}
+
+extension String {
+ var plainTextShort: String {
+ var result: String
+
+ result = trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression, range: nil)
+ guard result.count > 240 else {
+ return result
+ }
+ return result.prefix(240).components(separatedBy: " ").dropLast().joined(separator: " ").appending("...")
+ }
+}
+
+extension EntryCategoryType {
+ var elClass: String {
+ switch self {
+ case .companies:
+ return "website"
+ case .design:
+ return "brush"
+ case .development:
+ return "cogs"
+ case .marketing:
+ return "bullhorn"
+ case .newsletters:
+ return "envelope"
+ case .podcasts:
+ return "podcast"
+ case .updates:
+ return "file-new"
+ case .youtube:
+ return "video"
+ }
+ }
+}
+
+extension EntryCategory {
+ var elClass: String {
+ return type.elClass
+ }
+}
+
+struct HTMLController {
+ let views: [String: Markdown]
+
+ init(views: [String: Markdown]?) {
+ self.views = views ?? [String: Markdown]()
+ }
+
+ func category(req: Request) throws -> EventLoopFuture {
+ guard let category = req.parameters.get("category") else {
+ throw Abort(.notFound)
+ }
+
+ return Entry.query(on: req.db)
+ .with(\.$channel) { builder in
+ builder.with(\.$podcasts).with(\.$youtubeChannels)
+ }
+ .join(parent: \.$channel)
+ .with(\.$podcastEpisodes)
+ .join(children: \.$podcastEpisodes, method: .left)
+ .with(\.$youtubeVideos)
+ .join(children: \.$youtubeVideos, method: .left)
+ .filter(Channel.self, \Channel.$category.$id == category)
+ .filter(Channel.self, \Channel.$language.$id == "en")
+ .sort(\.$publishedAt, .descending)
+ .limit(32)
+ .all()
+ .flatMapThrowing { (entries) -> [Entry] in
+ guard entries.count > 0 else {
+ throw Abort(.notFound)
+ }
+ return entries
+ }
+ .flatMapEachThrowing {
+ try EntryItem(entry: $0)
+ }
+ .map { (items) -> HTML in
+ HTML(
+ .head(withSubtitle: "Swift Articles and News"),
+ .body(
+ .header(),
+ .main(
+ .class("container"),
+ .filters(),
+ .section(
+ .class("row"),
+ .ul(
+ .class("articles column"),
+ .forEach(items) {
+ .li(forEntryItem: $0)
+ }
+ )
+ )
+ )
+ )
+ )
+ }
+ }
+
+ func page(req: Request) -> EventLoopFuture {
+ guard let name = req.parameters.get("page") else {
+ return req.eventLoop.makeFailedFuture(Abort(.notFound))
+ }
+
+ guard let view = views[name] else {
+ return req.eventLoop.makeFailedFuture(Abort(.notFound))
+ }
+
+ let html = HTML(
+ .head(withSubtitle: "Support and FAQ"),
+ .body(
+ .header(),
+ .main(
+ .class("container"),
+ .filters(),
+ .section(
+ .class("row"),
+ .raw(view.html)
+ )
+ )
+ )
+ )
+
+ return req.eventLoop.future(html)
+ }
+
+ func index(req: Request) -> EventLoopFuture {
+ return Entry.query(on: req.db).join(LatestEntry.self, on: \Entry.$id == \LatestEntry.$id)
+ .with(\.$channel) { builder in
+ builder.with(\.$podcasts).with(\.$youtubeChannels)
+ }
+ .join(parent: \.$channel)
+ .with(\.$podcastEpisodes)
+ .join(children: \.$podcastEpisodes, method: .left)
+ .with(\.$youtubeVideos)
+ .join(children: \.$youtubeVideos, method: .left)
+ .filter(Channel.self, \Channel.$category.$id != "updates")
+ .filter(Channel.self, \Channel.$language.$id == "en")
+ .sort(\.$publishedAt, .descending)
+ .limit(32)
+ .all()
+ .flatMapEachThrowing {
+ try EntryItem(entry: $0)
+ }
+ .map { (items) -> HTML in
+ HTML(
+ .head(withSubtitle: "Swift Articles and News"),
+ .body(
+ .header(),
+ .main(
+ .class("container"),
+ .filters(),
+ .section(
+ .class("row"),
+ .ul(
+ .class("articles column"),
+ .forEach(items) {
+ .li(forEntryItem: $0)
+ }
+ )
+ )
+ )
+ )
+ )
+ }
+ }
+}
+
+extension HTMLController: RouteCollection {
+ func boot(routes: RoutesBuilder) throws {
+ routes.get("", use: index)
+ routes.get("category", ":category", use: category)
+ routes.get(":page", use: page)
+ }
+}
diff --git a/Sources/OrchardNestServer/Models/ChannelFeedItemsConfiguration.swift b/Sources/OrchardNestServer/Models/ChannelFeedItemsConfiguration.swift
new file mode 100644
index 0000000..69bdff9
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/ChannelFeedItemsConfiguration.swift
@@ -0,0 +1,45 @@
+import OrchardNestKit
+
+struct ChannelFeedItemsConfiguration {
+ let channel: Channel
+ let youtubeId: String?
+ let items: [FeedItem]
+
+ init(channels: [String: Channel], feedArgs: FeedConfiguration) {
+ let channel: Channel
+ if let oldChannel = channels[feedArgs.channel.feedUrl.absoluteString] {
+ channel = oldChannel
+ } else {
+ channel = Channel()
+ }
+ channel.title = feedArgs.channel.title
+ channel.$language.id = feedArgs.languageCode
+ channel.$category.id = feedArgs.categorySlug
+ channel.subtitle = feedArgs.channel.summary
+ channel.author = feedArgs.channel.author
+ channel.siteUrl = feedArgs.channel.siteUrl.absoluteString
+ channel.feedUrl = feedArgs.channel.feedUrl.absoluteString
+ channel.twitterHandle = feedArgs.channel.twitterHandle
+ channel.imageURL = feedArgs.channel.image?.absoluteString
+
+ channel.publishedAt = feedArgs.channel.updated
+
+ self.channel = channel
+ items = feedArgs.channel.items
+ youtubeId = feedArgs.channel.ytId
+ }
+}
+
+extension ChannelFeedItemsConfiguration {
+ func feedItems() throws -> [FeedItemConfiguration] {
+ let channelId = try channel.requireID()
+ return items.map { FeedItemConfiguration(channelId: channelId, feedItem: $0) }
+ }
+
+ var youtubeChannel: YouTubeChannel? {
+ guard let id = channel.id, let youtubeId = self.youtubeId else {
+ return nil
+ }
+ return YouTubeChannel(channelId: id, youtubeId: youtubeId)
+ }
+}
diff --git a/Sources/OrchardNestServer/Models/DB/Category.swift b/Sources/OrchardNestServer/Models/DB/Category.swift
new file mode 100644
index 0000000..6ea8505
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/DB/Category.swift
@@ -0,0 +1,29 @@
+import Fluent
+import Vapor
+
+final class Category: Model {
+ static var schema = "categories"
+
+ init() {}
+
+ init(slug: String) {
+ id = slug
+ }
+
+ @ID(custom: "slug", generatedBy: .user)
+ var id: String?
+}
+
+extension Category {
+ static func from(_ slug: String, on database: Database) -> EventLoopFuture {
+ Category.find(slug, on: database).flatMap { (langOpt) -> EventLoopFuture in
+ let category: Category
+ if let actual = langOpt {
+ category = actual
+ } else {
+ category = Category(slug: slug)
+ }
+ return category.save(on: database).transform(to: category)
+ }
+ }
+}
diff --git a/Sources/OrchardNestServer/Models/DB/CategoryTitle.swift b/Sources/OrchardNestServer/Models/DB/CategoryTitle.swift
new file mode 100644
index 0000000..3da80fc
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/DB/CategoryTitle.swift
@@ -0,0 +1,26 @@
+import Fluent
+import Vapor
+
+final class CategoryTitle: Model {
+ static var schema = "category_titles"
+
+ init() {}
+
+ init(id: UUID? = nil, language: Language, category: Category) throws {
+ self.id = id
+ $category.id = try category.requireID()
+ $language.id = try language.requireID()
+ }
+
+ @ID()
+ var id: UUID?
+
+ @Parent(key: "code")
+ var language: Language
+
+ @Parent(key: "slug")
+ var category: Category
+
+ @Field(key: "title")
+ var title: String
+}
diff --git a/Sources/OrchardNestServer/Models/DB/Channel.swift b/Sources/OrchardNestServer/Models/DB/Channel.swift
new file mode 100644
index 0000000..8af3112
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/DB/Channel.swift
@@ -0,0 +1,66 @@
+import Fluent
+import Vapor
+
+final class Channel: Model {
+ static var schema = "channels"
+
+ init() {}
+
+ @ID()
+ var id: UUID?
+
+ @Field(key: "title")
+ var title: String
+
+ @Parent(key: "language_code")
+ var language: Language
+
+ @Parent(key: "category_slug")
+ var category: Category
+
+ @OptionalField(key: "subtitle")
+ var subtitle: String?
+
+ @Field(key: "author")
+ var author: String
+
+ @Field(key: "site_url")
+ var siteUrl: String
+
+ @Field(key: "feed_url")
+ var feedUrl: String
+
+ @OptionalField(key: "twitter_handle")
+ var twitterHandle: String?
+
+ @OptionalField(key: "image")
+ var imageURL: String?
+
+ @Field(key: "published_at")
+ var publishedAt: Date
+
+ // When this Planet was created.
+ @Timestamp(key: "created_at", on: .create)
+ var createdAt: Date?
+
+ // When this Planet was last updated.
+ @Timestamp(key: "updated_at", on: .update)
+ var updatedAt: Date?
+
+ @Children(for: \.$channel)
+ var entries: [Entry]
+
+ @Children(for: \.$channel)
+ var podcasts: [PodcastChannel]
+
+ @Children(for: \.$channel)
+ var youtubeChannels: [YouTubeChannel]
+}
+
+extension Channel: Validatable {
+ static func validations(_ validations: inout Validations) {
+ validations.add("siteUrl", as: URL.self)
+ validations.add("feedUrl", as: URL.self)
+ validations.add("imageURL", as: URL.self)
+ }
+}
diff --git a/Sources/OrchardNestServer/Models/DB/ChannelStatus.swift b/Sources/OrchardNestServer/Models/DB/ChannelStatus.swift
new file mode 100644
index 0000000..9557ea0
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/DB/ChannelStatus.swift
@@ -0,0 +1,23 @@
+import Fluent
+import Vapor
+
+enum ChannelStatusType: String, Codable, CaseIterable {
+ case ignore
+}
+
+final class ChannelStatus: Model {
+ static var schema = "channel_statuses"
+
+ init() {}
+
+ init(feedUrl: URL, status: ChannelStatusType) {
+ id = feedUrl.absoluteString
+ self.status = status
+ }
+
+ @ID(custom: "feed_url", generatedBy: .user)
+ var id: String?
+
+ @Enum(key: "status")
+ var status: ChannelStatusType
+}
diff --git a/Sources/OrchardNestServer/Models/DB/Entry.swift b/Sources/OrchardNestServer/Models/DB/Entry.swift
new file mode 100644
index 0000000..1468195
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/DB/Entry.swift
@@ -0,0 +1,64 @@
+import Fluent
+import Vapor
+
+final class Entry: Model, Content {
+ static var schema = "entries"
+
+ init() {}
+
+ @ID()
+ var id: UUID?
+
+ @Parent(key: "channel_id")
+ var channel: Channel
+
+ @Field(key: "feed_id")
+ var feedId: String
+
+ @Field(key: "title")
+ var title: String
+
+ @Field(key: "summary")
+ var summary: String
+
+ @OptionalField(key: "content")
+ var content: String?
+
+ @Field(key: "url")
+ var url: String
+
+ @OptionalField(key: "image")
+ var imageURL: String?
+
+ @Field(key: "published_at")
+ var publishedAt: Date
+
+ // When this Planet was created.
+ @Timestamp(key: "created_at", on: .create)
+ var createdAt: Date?
+
+ // When this Planet was last updated.
+ @Timestamp(key: "updated_at", on: .update)
+ var updatedAt: Date?
+
+ @Children(for: \.$entry)
+ var podcastEpisodes: [PodcastEpisode]
+
+ var podcastEpisode: PodcastEpisode? {
+ return podcastEpisodes.first
+ }
+
+ @Children(for: \.$entry)
+ var youtubeVideos: [YoutubeVideo]
+
+ var youtubeVideo: YoutubeVideo? {
+ return youtubeVideos.first
+ }
+}
+
+extension Entry: Validatable {
+ static func validations(_ validations: inout Validations) {
+ validations.add("url", as: URL.self)
+ validations.add("imageURL", as: URL.self)
+ }
+}
diff --git a/Sources/OrchardNestServer/Models/DB/Language.swift b/Sources/OrchardNestServer/Models/DB/Language.swift
new file mode 100644
index 0000000..0e97839
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/DB/Language.swift
@@ -0,0 +1,34 @@
+import Fluent
+import Vapor
+
+final class Language: Model {
+ static var schema = "languages"
+
+ init() {}
+
+ init(code: String, title: String) {
+ id = code
+ self.title = title
+ }
+
+ @ID(custom: "code", generatedBy: .user)
+ var id: String?
+
+ @Field(key: "title")
+ var title: String
+}
+
+extension Language {
+ static func from(_ pair: (String, String), on database: Database) -> EventLoopFuture {
+ Language.find(pair.0, on: database).flatMap { (langOpt) -> EventLoopFuture in
+ let language: Language
+ if let actual = langOpt {
+ actual.title = pair.1
+ language = actual
+ } else {
+ language = Language(code: pair.0, title: pair.1)
+ }
+ return language.save(on: database).transform(to: language)
+ }
+ }
+}
diff --git a/Sources/OrchardNestServer/Models/DB/LatestEntry.swift b/Sources/OrchardNestServer/Models/DB/LatestEntry.swift
new file mode 100644
index 0000000..c281084
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/DB/LatestEntry.swift
@@ -0,0 +1,12 @@
+import Fluent
+import Vapor
+
+final class LatestEntry: Model {
+ static let schema = "latest_entries"
+
+ @ID()
+ var id: UUID?
+
+ @Field(key: "channel_id")
+ var channelId: UUID
+}
diff --git a/Sources/OrchardNestServer/Models/DB/PodcastChannel.swift b/Sources/OrchardNestServer/Models/DB/PodcastChannel.swift
new file mode 100644
index 0000000..13369af
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/DB/PodcastChannel.swift
@@ -0,0 +1,43 @@
+import Fluent
+import Vapor
+
+final class PodcastChannel: Model {
+ static var schema = "podcast_channels"
+
+ init() {}
+
+ init(channelId: UUID, appleId: Int) {
+ id = channelId
+ self.appleId = appleId
+ }
+
+ @ID(custom: "channel_id", generatedBy: .user)
+ var id: UUID?
+
+ @Field(key: "apple_id")
+ var appleId: Int
+
+ @Parent(key: "channel_id")
+ var channel: Channel
+}
+
+extension PodcastChannel {
+ static func upsert(_ newChannel: PodcastChannel, on database: Database) -> EventLoopFuture {
+ PodcastChannel.find(newChannel.id, on: database)
+ .optionalMap { $0.appleId == newChannel.appleId ? $0 : nil }
+ .flatMap { (channel) -> EventLoopFuture in
+ guard let channelId = newChannel.id, channel == nil else {
+ return database.eventLoop.makeSucceededFuture(())
+ }
+
+ return PodcastChannel.query(on: database).group(.or) {
+ $0.filter(\.$id == channelId).filter(\.$appleId == newChannel.appleId)
+ }.all().flatMapEach(on: database.eventLoop) { channel in
+ channel.delete(on: database)
+ }.flatMap { _ in
+ // context.logger.info("saving yt channel \"\(newChannel.youtubeId)\"")
+ newChannel.save(on: database)
+ }
+ }
+ }
+}
diff --git a/Sources/OrchardNestServer/Models/DB/PodcastEpisode.swift b/Sources/OrchardNestServer/Models/DB/PodcastEpisode.swift
new file mode 100644
index 0000000..7ce4c8b
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/DB/PodcastEpisode.swift
@@ -0,0 +1,45 @@
+import Fluent
+import Vapor
+
+final class PodcastEpisode: Model {
+ static var schema = "podcast_episodes"
+
+ init() {}
+
+ init(entryId: UUID, audioURL: String) {
+ id = entryId
+ self.audioURL = audioURL
+ }
+
+ @ID(custom: "entry_id", generatedBy: .user)
+ var id: UUID?
+
+ @Field(key: "audio")
+ var audioURL: String
+
+ @Parent(key: "entry_id")
+ var entry: Entry
+}
+
+extension PodcastEpisode: Validatable {
+ static func validations(_ validations: inout Validations) {
+ validations.add("audioURL", as: URL.self)
+ }
+}
+
+extension PodcastEpisode {
+ static func upsert(_ newEpisode: PodcastEpisode, on database: Database) -> EventLoopFuture {
+ return PodcastEpisode.find(newEpisode.id, on: database)
+ .flatMap { (episode) -> EventLoopFuture in
+ let savingEpisode: PodcastEpisode
+ if let oldEpisode = episode {
+ oldEpisode.audioURL = newEpisode.audioURL
+ savingEpisode = oldEpisode
+ } else {
+ savingEpisode = newEpisode
+ }
+ // context.logger.info("saving podcast episode \"\(savingEpisode.audioURL)\"")
+ return savingEpisode.save(on: database)
+ }
+ }
+}
diff --git a/Sources/OrchardNestServer/Models/DB/YouTubeChannel.swift b/Sources/OrchardNestServer/Models/DB/YouTubeChannel.swift
new file mode 100644
index 0000000..5209bb7
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/DB/YouTubeChannel.swift
@@ -0,0 +1,43 @@
+import Fluent
+import Vapor
+
+final class YouTubeChannel: Model {
+ static var schema = "youtube_channels"
+
+ init() {}
+
+ init(channelId: UUID, youtubeId: String) {
+ id = channelId
+ self.youtubeId = youtubeId
+ }
+
+ @ID(custom: "channel_id", generatedBy: .user)
+ var id: UUID?
+
+ @Field(key: "youtube_id")
+ var youtubeId: String
+
+ @Parent(key: "channel_id")
+ var channel: Channel
+}
+
+extension YouTubeChannel {
+ static func upsert(_ newChannel: YouTubeChannel, on database: Database) -> EventLoopFuture {
+ YouTubeChannel.find(newChannel.id, on: database)
+ .optionalMap { $0.youtubeId == newChannel.youtubeId ? $0 : nil }
+ .flatMap { (channel) -> EventLoopFuture in
+ guard let channelId = newChannel.id, channel == nil else {
+ return database.eventLoop.makeSucceededFuture(())
+ }
+
+ return YouTubeChannel.query(on: database).group(.or) {
+ $0.filter(\.$id == channelId).filter(\.$youtubeId == newChannel.youtubeId)
+ }.all().flatMapEach(on: database.eventLoop) { channel in
+ channel.delete(on: database)
+ }.flatMap { _ in
+ // context.logger.info("saving yt channel \"\(newChannel.youtubeId)\"")
+ newChannel.save(on: database)
+ }
+ }
+ }
+}
diff --git a/Sources/OrchardNestServer/Models/DB/YouTubeVideo.swift b/Sources/OrchardNestServer/Models/DB/YouTubeVideo.swift
new file mode 100644
index 0000000..4219de6
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/DB/YouTubeVideo.swift
@@ -0,0 +1,43 @@
+import Fluent
+import Vapor
+
+final class YoutubeVideo: Model {
+ static var schema = "youtube_videos"
+
+ init() {}
+
+ init(entryId: UUID, youtubeId: String) {
+ id = entryId
+ self.youtubeId = youtubeId
+ }
+
+ @ID(custom: "entry_id", generatedBy: .user)
+ var id: UUID?
+
+ @Field(key: "youtube_id")
+ var youtubeId: String
+
+ @Parent(key: "entry_id")
+ var entry: Entry
+}
+
+extension YoutubeVideo {
+ static func upsert(_ newVideo: YoutubeVideo, on database: Database) -> EventLoopFuture {
+ return YoutubeVideo.find(newVideo.id, on: database)
+ .optionalMap { $0.youtubeId == newVideo.youtubeId ? $0 : nil }
+ .flatMap { (video) -> EventLoopFuture in
+ guard let entryId = newVideo.id, video == nil else {
+ return database.eventLoop.makeSucceededFuture(())
+ }
+
+ return YoutubeVideo.query(on: database).group(.or) {
+ $0.filter(\.$id == entryId).filter(\.$youtubeId == newVideo.youtubeId)
+ }.all().flatMapEach(on: database.eventLoop) { channel in
+ channel.delete(on: database)
+ }.flatMap { _ in
+ // context.logger.info("saving yt video \"\(newVideo.youtubeId)\"")
+ newVideo.save(on: database)
+ }
+ }
+ }
+}
diff --git a/Sources/OrchardNestServer/Models/FeedChannel.swift b/Sources/OrchardNestServer/Models/FeedChannel.swift
new file mode 100644
index 0000000..2d6ab31
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/FeedChannel.swift
@@ -0,0 +1,70 @@
+import OrchardNestKit
+import Vapor
+
+#if canImport(FoundationNetworking)
+ import FoundationNetworking
+#endif
+
+struct EmptyError: Error {}
+
+extension FeedChannel {
+ static func parseSite(_ site: OrganizedSite, using client: Client, on eventLoop: EventLoop) -> EventLoopFuture> {
+ // let uri = URI(string: site.site.feed_url.absoluteString)
+ // let headers = HTTPHeaders([("Host", uri.host!), ("User-Agent", "OrchardNest-Robot"), ("Accept", "*/*")])
+
+ // let promise = eventLoop.makePromise(of: Result.self)
+ return client.get(URI(string: site.site.feed_url.absoluteString)).map { (response) -> Data? in
+ response.body.map { buffer in
+ Data(buffer: buffer)
+ }
+ }.flatMapAlways { (result) -> EventLoopFuture> in
+ let newResult = result.mapError { FeedError.download(site.site.feed_url, $0) }.flatMap { (data) -> Result in
+ guard let data = data else {
+ return .failure(.empty(site.site.feed_url))
+ }
+ return .success(data)
+ }.flatMap { data in
+ Result {
+ try FeedChannel(language: site.languageCode, category: site.categorySlug, site: site.site, data: data)
+ }.mapError { .parser(site.site.feed_url, $0) }
+ }.flatMap { (channel) -> Result in
+ guard channel.items.count > 0 || channel.itemCount == channel.items.count else {
+ return .failure(.items(site.site.feed_url))
+ }
+ return .success(channel)
+ }
+ return eventLoop.future(newResult)
+ }
+// URLSession.shared.dataTask(with: site.site.feed_url) { data, _, error in
+// let result: Result
+// if let error = error {
+// result = .failure(error)
+// } else if let data = data {
+// result = .success(data)
+// } else {
+// promise.fail(EmptyError())
+// return
+// }
+// promise.succeed(result)
+// }.resume()
+// return promise.futureResult.flatMap { (result) -> EventLoopFuture> in
+//
+// let responseBody: Data
+// do {
+// responseBody = try result.get()
+// } catch {
+// return eventLoop.future(.failure(.download(site.site.feed_url, error)))
+// }
+// let channel: FeedChannel
+// do {
+// channel = try FeedChannel(language: site.languageCode, category: site.categorySlug, site: site.site, data: responseBody)
+// } catch {
+// return eventLoop.future(.failure(.parser(site.site.feed_url, error)))
+// }
+// guard channel.items.count > 0 || channel.itemCount == channel.items.count else {
+// return eventLoop.future(.failure(.items(site.site.feed_url)))
+// }
+// return eventLoop.future(.success(channel))
+// }
+ }
+}
diff --git a/Sources/OrchardNestServer/Models/FeedConfiguration.swift b/Sources/OrchardNestServer/Models/FeedConfiguration.swift
new file mode 100644
index 0000000..8b2252d
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/FeedConfiguration.swift
@@ -0,0 +1,25 @@
+import OrchardNestKit
+
+struct FeedConfiguration {
+ let categorySlug: String
+ let languageCode: String
+ let channel: FeedChannel
+}
+
+extension FeedConfiguration {
+ static func from(
+ categorySlug: String,
+ languageCode: String,
+ channel: FeedChannel,
+ langMap: [String: Language],
+ catMap: [String: Category]
+ ) -> FeedResult {
+ guard let newLangId = langMap[languageCode]?.id else {
+ return .failure(.invalidParent(channel.feedUrl, languageCode))
+ }
+ guard let newCatId = catMap[categorySlug]?.id else {
+ return .failure(.invalidParent(channel.feedUrl, categorySlug))
+ }
+ return .success(FeedConfiguration(categorySlug: newCatId, languageCode: newLangId, channel: channel))
+ }
+}
diff --git a/Sources/OrchardNestServer/Models/FeedError.swift b/Sources/OrchardNestServer/Models/FeedError.swift
new file mode 100644
index 0000000..e54dfeb
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/FeedError.swift
@@ -0,0 +1,24 @@
+import Foundation
+
+enum FeedError: Error {
+ case download(URL, Error)
+ case empty(URL)
+ case parser(URL, Error)
+ case items(URL)
+ case invalidParent(URL, String)
+
+ var localizedDescription: String {
+ switch self {
+ case let .download(url, error):
+ return "\(url), download, \"\(error)\""
+ case let .empty(url):
+ return "\(url), empty"
+ case let .invalidParent(url, parent):
+ return "\(url), parent, \(parent)"
+ case let .items(url):
+ return "\(url), items"
+ case let .parser(url, error):
+ return "\(url), parser, \"\(error)\""
+ }
+ }
+}
diff --git a/Sources/OrchardNestServer/Models/FeedItemConfiguration.swift b/Sources/OrchardNestServer/Models/FeedItemConfiguration.swift
new file mode 100644
index 0000000..1662018
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/FeedItemConfiguration.swift
@@ -0,0 +1,7 @@
+import Foundation
+import OrchardNestKit
+
+struct FeedItemConfiguration {
+ let channelId: UUID
+ let feedItem: FeedItem
+}
diff --git a/Sources/OrchardNestServer/Models/FeedItemEntry.swift b/Sources/OrchardNestServer/Models/FeedItemEntry.swift
new file mode 100644
index 0000000..4884729
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/FeedItemEntry.swift
@@ -0,0 +1,46 @@
+import Fluent
+import OrchardNestKit
+import Vapor
+
+struct FeedItemEntry {
+ let entry: Entry
+ let feedItem: FeedItem
+
+ static func from(upsertOn database: Database, from config: FeedItemConfiguration) -> EventLoopFuture {
+ Entry.query(on: database).filter(\.$feedId == config.feedItem.id).first().flatMap { foundEntry in
+ let newEntry: Entry
+ if let entry = foundEntry {
+ newEntry = entry
+ } else {
+ newEntry = Entry()
+ }
+
+ newEntry.$channel.id = config.channelId
+ newEntry.content = config.feedItem.content
+ newEntry.feedId = config.feedItem.id
+ newEntry.imageURL = config.feedItem.image?.absoluteString
+ newEntry.publishedAt = config.feedItem.published
+ newEntry.summary = config.feedItem.summary
+ newEntry.title = config.feedItem.title
+ newEntry.url = config.feedItem.url.absoluteString
+ // context.logger.info("saving entry for \"\(config.feedItem.url)\"")
+ return newEntry.save(on: database).transform(to: Self(entry: newEntry, feedItem: config.feedItem))
+ }
+ }
+}
+
+extension FeedItemEntry {
+ var podcastEpisode: PodcastEpisode? {
+ guard let id = entry.id, let audioURL = feedItem.audio else {
+ return nil
+ }
+ return PodcastEpisode(entryId: id, audioURL: audioURL.absoluteString)
+ }
+
+ var youtubeVideo: YoutubeVideo? {
+ guard let id = entry.id, let youtubeId = feedItem.ytId else {
+ return nil
+ }
+ return YoutubeVideo(entryId: id, youtubeId: youtubeId)
+ }
+}
diff --git a/Sources/OrchardNestServer/Models/FeedResult.swift b/Sources/OrchardNestServer/Models/FeedResult.swift
new file mode 100644
index 0000000..171c1c6
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/FeedResult.swift
@@ -0,0 +1 @@
+typealias FeedResult = Result
diff --git a/Sources/OrchardNestServer/Models/Model.swift b/Sources/OrchardNestServer/Models/Model.swift
new file mode 100644
index 0000000..af3e9d3
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/Model.swift
@@ -0,0 +1,11 @@
+import Fluent
+
+extension Model {
+ static func dictionary(from elements: [Self]) -> [IDType: Self] where IDType == Self.IDValue {
+ return Dictionary(uniqueKeysWithValues:
+ elements.compactMap { model in
+ model.id.map { ($0, model) }
+ }
+ )
+ }
+}
diff --git a/Sources/OrchardNestServer/Models/SiteCatalogMap.swift b/Sources/OrchardNestServer/Models/SiteCatalogMap.swift
new file mode 100644
index 0000000..b6b49f6
--- /dev/null
+++ b/Sources/OrchardNestServer/Models/SiteCatalogMap.swift
@@ -0,0 +1,29 @@
+import OrchardNestKit
+
+struct SiteCatalogMap {
+ let languages: [String: String]
+ let categories: [String: [String: String]]
+ let organizedSites: [OrganizedSite]
+
+ init(sites: [LanguageContent]) {
+ var languages = [String: String]()
+ var categories = [String: [String: String]]()
+ var organizedSites = [OrganizedSite]()
+
+ for lang in sites {
+ languages[lang.language] = lang.title
+ for category in lang.categories {
+ var categoryMap = categories[category.slug] ?? [String: String]()
+ categoryMap[lang.language] = category.title
+ categories[category.slug] = categoryMap
+ organizedSites.append(contentsOf: category.sites.map {
+ OrganizedSite(languageCode: lang.language, categorySlug: category.slug, site: $0)
+ })
+ }
+ }
+
+ self.categories = categories
+ self.languages = languages
+ self.organizedSites = organizedSites
+ }
+}
diff --git a/Sources/OrchardNestServer/OrchardNest.swift b/Sources/OrchardNestServer/OrchardNest.swift
deleted file mode 100644
index b468250..0000000
--- a/Sources/OrchardNestServer/OrchardNest.swift
+++ /dev/null
@@ -1,71 +0,0 @@
-import Fluent
-import FluentPostgresDriver
-import Vapor
-
-public protocol ConfiguratorProtocol {
- func configure(_ app: Application) throws
-}
-
-//
-public final class Configurator: ConfiguratorProtocol {
- public static let shared: ConfiguratorProtocol = Configurator()
-
- //
- ///// Called before your application initializes.
- public func configure(_ app: Application) throws {
- // Register providers first
- // try services.register(FluentPostgreSQLProvider())
- // try services.register(AuthenticationProvider())
-
- // services.register(DirectoryIndexMiddleware.self)
-
- // Register middleware
- // var middlewares = MiddlewareConfig() // Create _empty_ middleware config
- // middlewares.use(SessionsMiddleware.self) // Enables sessions.
- let rootPath = Environment.get("ROOT_PATH") ?? app.directory.publicDirectory
-
-// app.webSockets = WebSocketRepository()
-//
-// app.middleware.use(DirectoryIndexMiddleware(publicDirectory: rootPath))
-
- app.middleware.use(ErrorMiddleware.default(environment: app.environment))
- // middlewares.use(ErrorMiddleware.self) // Catches errors and converts to HTTP response
- // services.register(middlewares)
-
- // Configure a SQLite database
- let postgreSQLConfig: PostgresConfiguration
-
- if let url = Environment.get("DATABASE_URL") {
- postgreSQLConfig = PostgresConfiguration(url: url)!
- } else {
- postgreSQLConfig = PostgresConfiguration(hostname: "localhost", username: "orchardnest")
- }
-
- app.databases.use(.postgres(configuration: postgreSQLConfig), as: .psql)
-
-// app.databases.middleware.use(UserEmailerMiddleware(app: app))
-//
-// app.migrations.add(CreateDevice())
-// app.migrations.add(CreateAppleUser())
-// app.migrations.add(CreateDeviceWorkout())
-// app.migrations.add(ActivateWorkout())
- // let wss = NIOWebSocketServer.default()
-
-// app.webSocket("api", "v1", "workouts", ":id", "listen") { req, websocket in
-// guard let idData = try? Base32CrockfordEncoding.encoding.decode(base32Encoded: req.parameters.get("id")!) else {
-// return
-// }
-// let workoutID = UUID(data: idData)
-//
-// _ = Workout.find(workoutID, on: req.db).unwrap(or: Abort(HTTPResponseStatus.notFound)).flatMapThrowing { workout in
-// let workoutId = try workout.requireID()
-// app.webSockets.save(websocket, withID: workoutId)
-// }
-// }
-
- // services.register(wss, as: WebSocketServer.self)
- app.get { _ in
- "Hello"
- }
- }
-}
diff --git a/Sources/OrchardNestServer/RefreshCommand.swift b/Sources/OrchardNestServer/RefreshCommand.swift
new file mode 100644
index 0000000..9ada1fb
--- /dev/null
+++ b/Sources/OrchardNestServer/RefreshCommand.swift
@@ -0,0 +1,9 @@
+import Vapor
+
+struct RefreshCommand: Command {
+ typealias Signature = RefreshConfiguration
+
+ var help: String
+
+ func run(using _: CommandContext, signature _: RefreshConfiguration) throws {}
+}
diff --git a/Sources/OrchardNestServer/RefreshConfiguration.swift b/Sources/OrchardNestServer/RefreshConfiguration.swift
new file mode 100644
index 0000000..91605f3
--- /dev/null
+++ b/Sources/OrchardNestServer/RefreshConfiguration.swift
@@ -0,0 +1,5 @@
+import Vapor
+
+struct RefreshConfiguration: CommandSignature, Codable {
+ init() {}
+}
diff --git a/Sources/OrchardNestServer/RefreshJob.swift b/Sources/OrchardNestServer/RefreshJob.swift
new file mode 100644
index 0000000..fe2ece3
--- /dev/null
+++ b/Sources/OrchardNestServer/RefreshJob.swift
@@ -0,0 +1,260 @@
+import Fluent
+import NIO
+import OrchardNestKit
+import Queues
+import Vapor
+
+struct ApplePodcastResult: Codable {
+ let collectionId: Int
+}
+
+struct ApplePodcastResponse: Codable {
+ let results: [ApplePodcastResult]
+}
+
+struct RefreshJob: ScheduledJob, Job {
+ func run(context: QueueContext) -> EventLoopFuture {
+ context.queue.dispatch(
+ RefreshJob.self,
+ RefreshConfiguration()
+ )
+ }
+
+ static let url = URL(string: "https://raw.githubusercontent.com/daveverwer/iOSDevDirectory/master/blogs.json")!
+
+ static let basePodcastQueryURLComponents = URLComponents(string: """
+ https://itunes.apple.com/search?media=podcast&attribute=titleTerm&limit=1&entity=podcast
+ """)!
+
+ static func queryURL(forPodcastWithTitle title: String) -> URI {
+ var components = Self.basePodcastQueryURLComponents
+ guard var queryItems = components.queryItems else {
+ preconditionFailure()
+ }
+ queryItems.append(URLQueryItem(name: "term", value: title.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)))
+ components.queryItems = queryItems
+ return URI(
+ scheme: components.scheme,
+ host: components.host,
+ port: components.port,
+ path: components.path,
+ query: components.query,
+ fragment: components.fragment
+ )
+ }
+
+ typealias Payload = RefreshConfiguration
+
+ func error(_ context: QueueContext, _ error: Error, _: RefreshConfiguration) -> EventLoopFuture {
+ context.logger.report(error: error)
+ return context.eventLoop.future()
+ }
+
+ // swiftlint:disable:next function_body_length
+ func dequeue(_ context: QueueContext, _: RefreshConfiguration) -> EventLoopFuture {
+ let database = context.application.db
+ let client = context.application.client
+
+ let decoder = JSONDecoder()
+
+ context.logger.info("downloading blog list...")
+
+ return context.application.client.get(URI(string: Self.url.absoluteString)).flatMapThrowing { (response) -> [LanguageContent] in
+ try response.content.decode([LanguageContent].self, using: decoder)
+ }.map(SiteCatalogMap.init).flatMap { (siteCatalogMap) -> EventLoopFuture in
+
+ let languages = siteCatalogMap.languages
+ let categories = siteCatalogMap.categories
+ let organizedSites = siteCatalogMap.organizedSites
+
+ let futureLanguages = languages.map { Language.from($0, on: database) }.flatten(on: database.eventLoop)
+ let futureCategories = categories.map { Category.from($0.key, on: database) }.flatten(on: database.eventLoop)
+
+ let langMap = futureLanguages.map(Language.dictionary(from:))
+ let catMap = futureCategories.map(Category.dictionary(from:))
+
+ // need map to lang, cats
+
+ let futureFeedResults: EventLoopFuture<[FeedResult]>
+ futureFeedResults = langMap.and(catMap).flatMap { (maps) -> EventLoopFuture<[FeedResult]> in
+ context.logger.info("downloading feeds...")
+
+ let (langMap, catMap) = maps
+ var results = [EventLoopFuture]()
+ let promise = context.eventLoop.makePromise(of: Void.self)
+ _ = context.eventLoop.scheduleRepeatedAsyncTask(
+ initialDelay: .seconds(1),
+ delay: .nanoseconds(20_000_000)
+ ) { (task: RepeatedTask) -> EventLoopFuture in
+ guard results.count < organizedSites.count else {
+ task.cancel(promise: promise)
+
+ context.logger.info("finished downloading feeds...")
+ return context.eventLoop.makeSucceededFuture(())
+ }
+ let args = organizedSites[results.count]
+ context.logger.info("downloading \"\(args.site.feed_url)\"")
+ let result = FeedChannel.parseSite(args, using: client, on: context.eventLoop).map { result in
+ result.flatMap { FeedConfiguration.from(
+ categorySlug: args.categorySlug,
+ languageCode: args.languageCode,
+ channel: $0,
+ langMap: langMap,
+ catMap: catMap
+ )
+ }
+ }
+ results.append(result)
+ return result.transform(to: ())
+ }
+ let finalResults = promise.futureResult.flatMap {
+ results.flatten(on: context.eventLoop)
+ }
+
+ return finalResults
+// return organizedSites.map { orgSite in
+// FeedChannel.parseSite(orgSite, using: context.application.client, on: context.eventLoop)
+// .map { result in
+// result.flatMap { FeedConfiguration.from(
+// categorySlug: orgSite.categorySlug,
+// languageCode: orgSite.languageCode,
+// channel: $0,
+// langMap: langMap,
+// catMap: catMap
+// )
+// }
+// }
+// }.flatten(on: context.eventLoop)
+ }
+
+ let groupedResults = futureFeedResults.map { results -> ([FeedConfiguration], [FeedError]) in
+ var errors = [FeedError]()
+ var configurations = [FeedConfiguration]()
+ results.forEach {
+ switch $0 {
+ case let .success(config): configurations.append(config)
+ case let .failure(error): errors.append(error)
+ }
+ }
+ return (configurations, errors)
+ }
+
+ groupedResults.whenSuccess { groupedResults in
+ let errors = groupedResults.1
+ for error in errors {
+ context.logger.info("\(error.localizedDescription)")
+ }
+ }
+
+ return database.transaction { database in
+ let futureFeeds = groupedResults.map { $0.0 }.map { configs -> [FeedConfiguration] in
+ let feeds = Dictionary(grouping: configs) { $0.channel.feedUrl }
+ return feeds.compactMap { $0.value.first }
+ }
+ let currentChannels = futureFeeds.map { (args) -> [String] in
+ args.map { $0.channel.feedUrl.absoluteString }
+ }.flatMap { feedUrls in
+ Channel.query(on: database).filter(\.$feedUrl ~~ feedUrls).with(\.$podcasts).all()
+ }.map {
+ Dictionary(uniqueKeysWithValues: ($0.map {
+ ($0.feedUrl, $0)
+ }))
+ }
+
+ let futureChannels = futureFeeds.and(currentChannels).map { (args) -> [ChannelFeedItemsConfiguration] in
+ context.logger.info("beginning upserting channels...")
+ let (feeds, currentChannels) = args
+
+ return feeds.map { feedArgs in
+ ChannelFeedItemsConfiguration(channels: currentChannels, feedArgs: feedArgs)
+ }
+ }.flatMap { (configurations) -> EventLoopFuture<[ChannelFeedItemsConfiguration]> in
+
+ database.withConnection { (database) -> EventLoopFuture<[ChannelFeedItemsConfiguration]> in
+
+ var results = [EventLoopFuture]()
+ let promise = context.eventLoop.makePromise(of: Void.self)
+ _ = context.eventLoop.scheduleRepeatedAsyncTask(
+ initialDelay: .seconds(1),
+ delay: .nanoseconds(20_000_000)
+ ) { (task: RepeatedTask) -> EventLoopFuture in
+ guard results.count < configurations.count else {
+ task.cancel(promise: promise)
+
+ context.logger.info("finished upserting channels...")
+ return context.eventLoop.makeSucceededFuture(())
+ }
+ let args = configurations[results.count]
+ context.logger.info("saving \"\(args.channel.title)\"")
+ let result = args.channel.save(on: database).transform(to: args).flatMapError { _ -> EventLoopFuture in
+ database.eventLoop.future(ChannelFeedItemsConfiguration?.none)
+ }
+ results.append(result)
+ return result.transform(to: ())
+ }
+ let finalResults = promise.futureResult.flatMap {
+ results.flatten(on: context.eventLoop).mapEachCompact { $0 }
+ }
+
+ return finalResults
+ }
+ }
+
+ let podcastChannels = futureChannels.mapEachCompact { (configuration) -> Channel? in
+ let hasPodcastEpisode = (configuration.items.first { $0.audio != nil }) != nil
+
+ guard hasPodcastEpisode || configuration.channel.$category.id == "podcasts" else {
+ return nil
+ }
+ if let podcasts = configuration.channel.$podcasts.value {
+ guard podcasts.count == 0 else {
+ return nil
+ }
+ }
+ return configuration.channel
+ }.flatMapEachCompact(on: context.eventLoop) { (channel) -> EventLoopFuture in
+ client.get(Self.queryURL(forPodcastWithTitle: channel.title)).flatMapThrowing {
+ try $0.content.decode(ApplePodcastResponse.self, using: decoder)
+ }.map { (response) -> (PodcastChannel?) in
+ response.results.first.flatMap { result in
+ channel.id.map { ($0, result.collectionId) }
+ }.map(PodcastChannel.init)
+ }.recover { _ in nil }
+ }.flatMapEach(on: context.eventLoop) { $0.create(on: database) }
+
+ // save youtube channels to channels
+ let futYTChannels = futureChannels.mapEachCompact { (channel) -> YouTubeChannel? in
+ channel.youtubeChannel
+ }.flatMapEach(on: database.eventLoop) { newChannel in
+ YouTubeChannel.upsert(newChannel, on: database)
+ }
+
+ // save entries to channels
+ let futureEntries = futureChannels
+ .flatMapEachThrowing { try $0.feedItems() }
+ .map { $0.flatMap { $0 } }
+ .flatMapEach(on: database.eventLoop) { (config) -> EventLoopFuture in
+ FeedItemEntry.from(upsertOn: database, from: config)
+ }
+
+ // save videos to entries
+ let futYTVideos = futureEntries.mapEachCompact { (entry) -> YoutubeVideo? in
+ entry.youtubeVideo
+ }.flatMapEach(on: database.eventLoop) { newVideo in
+ YoutubeVideo.upsert(newVideo, on: database)
+ }
+
+ // save podcastepisodes to entries
+
+ let futPodEpisodes = futureEntries.mapEachCompact { (entry) -> PodcastEpisode? in
+
+ entry.podcastEpisode
+ }.flatMapEach(on: database.eventLoop) { newEpisode in
+ PodcastEpisode.upsert(newEpisode, on: database)
+ }
+
+ return futYTVideos.and(futYTChannels).and(futPodEpisodes).and(podcastChannels).transform(to: ())
+ }
+ }
+ }
+}
diff --git a/Sources/orcnst-serve/main.swift b/Sources/orchardnestd/main.swift
similarity index 100%
rename from Sources/orcnst-serve/main.swift
rename to Sources/orchardnestd/main.swift
diff --git a/Sources/orcnst/main.swift b/Sources/orcnst/main.swift
deleted file mode 100644
index 642ea0c..0000000
--- a/Sources/orcnst/main.swift
+++ /dev/null
@@ -1,51 +0,0 @@
-import Foundation
-import OrchardNestKit
-
-typealias OrganizedSite = (String, String, Site)
-
-if true {
- let blogs = URL(string: "https://raw.githubusercontent.com/daveverwer/iOSDevDirectory/master/blogs.json")!
-
- let reader = BlogReader()
- let sites = try reader.sites(fromURL: blogs)
-
- let orgSites = sites.flatMap { (content) -> [OrganizedSite] in
- let language = content.language
- return content.categories.flatMap { (category) -> [OrganizedSite] in
- category.sites.map {
- (language, category.slug, $0)
- }
- }
- }
-
- let channelResults = orgSites.map { args in
- Result {
- try Channel(language: args.0, category: args.1, site: args.2)
- }
- }
-
- var errors = [Error]()
- var channels = [Channel]()
-
- for result in channelResults {
- switch result {
- case let .failure(error):
- errors.append(error)
- case let .success(channel):
- channels.append(channel)
- }
- }
-
- debugPrint(errors)
-
- let encoder = JSONEncoder()
- encoder.outputFormatting = .prettyPrinted
- let data = try encoder.encode(channels)
-
- try data.write(to:
- URL(fileURLWithPath: "/Users/leo/data.json")
- )
-
-} else {
- let data = try Data(contentsOf: URL(fileURLWithPath: "/Users/leo/Downloads/data.json"))
-}
diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift
index 7371d47..01dfd5c 100644
--- a/Tests/LinuxMain.swift
+++ b/Tests/LinuxMain.swift
@@ -1,7 +1,8 @@
import XCTest
-import OrchardNestTests
+import OrchardNestKitTests
var tests = [XCTestCaseEntry]()
-tests += OrchardNestTests.allTests()
+tests += OrchardNestKitTests.__allTests()
+
XCTMain(tests)
diff --git a/Tests/OrchardNestKitTests/XCTestManifests.swift b/Tests/OrchardNestKitTests/XCTestManifests.swift
index 16d86a1..d864e79 100644
--- a/Tests/OrchardNestKitTests/XCTestManifests.swift
+++ b/Tests/OrchardNestKitTests/XCTestManifests.swift
@@ -1,9 +1,18 @@
-import XCTest
-
#if !canImport(ObjectiveC)
- public func allTests() -> [XCTestCaseEntry] {
+ import XCTest
+
+ extension OrchardNestTests {
+ // DO NOT MODIFY: This is autogenerated, use:
+ // `swift test --generate-linuxmain`
+ // to regenerate.
+ static let __allTests__OrchardNestTests = [
+ ("testExample", testExample)
+ ]
+ }
+
+ public func __allTests() -> [XCTestCaseEntry] {
return [
- testCase(OrchardNestTests.allTests)
+ testCase(OrchardNestTests.__allTests__OrchardNestTests)
]
}
#endif
diff --git a/create_db.sql b/create_db.sql
new file mode 100644
index 0000000..43309fe
--- /dev/null
+++ b/create_db.sql
@@ -0,0 +1,3 @@
+create database orchardnest;
+create user orchardnest;
+grant all privileges on database orchardnest to orchardnest;
\ No newline at end of file
diff --git a/server.sh b/server.sh
new file mode 100644
index 0000000..15244ed
--- /dev/null
+++ b/server.sh
@@ -0,0 +1,37 @@
+apt update
+apt -y full-upgrade
+apt -y install tmux supervisor postgresql nginx zsh \
+ binutils \
+ git \
+ gnupg2 \
+ libc6-dev \
+ libcurl4 \
+ libedit2 \
+ libgcc-9-dev \
+ libpython2.7 \
+ libsqlite3-0 \
+ libstdc++-9-dev \
+ libxml2 \
+ libz3-dev \
+ pkg-config \
+ tzdata \
+ zlib1g-dev
+
+sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
+curl https://swift.org/builds/swift-5.2.5-release/ubuntu2004/swift-5.2.5-RELEASE/swift-5.2.5-RELEASE-ubuntu20.04.tar.gz | tar xzf - -C /usr/share/
+mv /usr/share/swift-5.2.5-RELEASE-ubuntu20.04 /usr/share/swift
+export PATH=/usr/share/swift/usr/bin:"${PATH}"
+
+# download swift
+
+# create db and user with password
+
+
+# as orchardnest user
+git clone https://github.com/brightdigit/OrchardNest.git app
+swift build -c release --enable-test-discovery
+
+sudo -u postgres createuser orchardnest
+sudo -u postgres createdb orchardnest
+sudo -u postgres psql -c "alter user orchardnest with encrypted password '12345';"
+sudo -u postgres psql -c "grant all privileges on database orchardnest to orchardnest;"