diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 0000000..8c752d4
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,49 @@
+name: Deploy book to Pages
+
+on:
+ push:
+ branches: ["main"]
+
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: "pages"
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ env:
+ MDBOOK_VERSION: 0.4
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install mdBook
+ run: |
+ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh
+ rustup update
+ cargo install --version ${MDBOOK_VERSION} mdbook
+ - name: Setup Pages
+ id: pages
+ uses: actions/configure-pages@v4
+ - name: Build with mdBook
+ run: mdbook build
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: ./book
+
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7585238
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+book
diff --git a/README.md b/README.md
index 0fab3d9..71620c5 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,16 @@
-# guide
-Selium user guide
+# Selium User Guide
+
+Selium's user guide, built by [`mdBook`](https://github.com/rust-lang/mdBook).
+
+## Editing
+
+To render the book whilst making changes, mdBook has a built in web server to help:
+
+```bash
+mdbook serve --open
+```
+
+## Deployment
+
+The rendered book is deployed to GitHub Pages using the `pages.yml` workflow. Any push to `main`
+will trigger this deployment.
diff --git a/book.toml b/book.toml
new file mode 100644
index 0000000..66ed946
--- /dev/null
+++ b/book.toml
@@ -0,0 +1,12 @@
+[book]
+authors = ["Selium Developers"]
+language = "en"
+multilingual = false
+src = "src"
+title = "Selium User Guide"
+
+[output.html]
+default-theme = "ayu"
+preferred-dark-theme = "ayu"
+git-repository-url = "https://github.com/seliumlabs/selium"
+cname = "guide.selium.com"
diff --git a/src/SUMMARY.md b/src/SUMMARY.md
new file mode 100644
index 0000000..035076d
--- /dev/null
+++ b/src/SUMMARY.md
@@ -0,0 +1,16 @@
+# Summary
+![Selium.com](selium.png)
+
+[Introduction](./introduction.md)
+
+# Getting Started
+
+- [First Steps](./getting_started/first_steps.md)
+- [Request/Reply](./getting_started/request_reply.md)
+- [Reusing Connections](./getting_started/reusing_connections.md)
+- [`Stream` and `Sink` traits](./getting_started/streams_and_sinks.md)
+- [Codecs](./getting_started/codecs.md)
+
+---
+
+- [Selium Cloud](./cloud.md)
diff --git a/src/cloud.md b/src/cloud.md
new file mode 100644
index 0000000..3bcaca9
--- /dev/null
+++ b/src/cloud.md
@@ -0,0 +1,77 @@
+# Selium Cloud
+
+Selium Cloud (Beta) consists of a managed Selium Server, authentication, certificate
+management, dashboard and technical support. If you want a hands-free, production-ready
+solution for all of your software comms, backed by the people that made Selium, this is
+for you.
+
+## Getting Started
+
+#### 1. Sign up
+To get started with Selium Cloud, you'll [need an account!](https://www.selium.com/#CTA)
+Selium Cloud is free for life, and super affordable as you grow with us.
+
+#### 2. Create your first certificate
+Once you've got your account, login to the Selium Cloud dashboard at
+[cloud.selium.io](https://cloud.selium.io). From here, you can create TLS certificates
+for each client on your account.
+
+After logging in, you should see a screen like this:
+
+
+
+To create a certificate, fill in a name for your client, e.g. "web1.example.com", then
+click the submit button. After a moment you should see a dialog box like this:
+
+
+
+**Make sure you download the private key as you cannot download it again!**
+
+Add the public and private keys to your project and you're ready to start using Selium.
+
+## Basic Usage
+
+Selium Cloud's API feels just like running your own Selium server. The main difference is
+when establishing a connection to the server. Instead of using `selium::custom()` to
+connect to your own server, use `selium::cloud()` to connect to the cloud:
+
+```rust
+let connection = selium::cloud()
+ .with_cert_and_key(
+ "./web1.example.com.der",
+ "./web1.example.com.key.der",
+ )?
+ .connect()
+ .await?;
+```
+
+Note that when using `selium::cloud()`, you won't need to specify a CA path or endpoint.
+These details are baked into the Selium client for your convenience.
+
+#### Namespaces
+
+Each Selium Cloud account is linked to a unique _namespace_. This is used to distinguish
+your data from other accounts, and is linked to every certificate you create. You can find
+your namespace on the Selium Cloud dashboard:
+
+
+
+To use your namespace, simply prepend it to every topic name. For example, to publish the
+topic "retail-transactions", you would code the following:
+
+```rust
+let mut publisher = connection
+ .publisher("/example/retail-transactions")
+ ...
+ .await?;
+```
+
+### IMPORTANT NOTE!
+
+**You must compile your code with the `--release` flag for both testing and production
+use. This is so the Selium client knows to use the _production_ Certificate Authority!**
+
+If you don't do this, you will not be able to connect to Selium.
+
+> N.B. We know this isn't ideal, and likely we'll be removing this restriction in future
+> versions. We'd love your feedback on this too!
diff --git a/src/getting_started/codecs.md b/src/getting_started/codecs.md
new file mode 100644
index 0000000..f3b00a3
--- /dev/null
+++ b/src/getting_started/codecs.md
@@ -0,0 +1,87 @@
+# Codecs
+Looking back at the previous chapters, you probably noticed lines like these ones:
+
+```rust
+let mut publisher = connection
+ ...
+ .with_encoder(StringCodec) // allows you to exchange string messages between clients
+
+let mut subscriber = connection
+ ...
+ .with_decoder(StringCodec) // use the same codec as the publisher
+```
+
+If you've not worked with data streaming before, codecs might seem like a very strange
+concept indeed. However the answer is quite simple. The term _codecs_ is a portmanteau of
+_encoder_ and _decoder_. Thus a codec is simply a structure that can _encode_ frames of
+data on one side of a connection, and _decode_ them on the other.
+
+In the example above, we use `StringCodec`, which does exactly what it says on the tin -
+it _encodes_ strings to bytes and then _decodes_ those bytes back to strings.
+
+## Why do we need codecs?
+
+We need codecs because at the network level, computers only support sending raw bytes.
+However most data we work with are not raw bytes - they're strings, integers, booleans,
+enumerators, structures etc. In order to abstract away the pain of converting these types
+to bytes and back again, we use a codec that knows how to do it for us.
+
+## Can I send more than just strings?
+
+Yep! With the help of the `serde` crate, we support sending all manner of Rust types.
+In the example below, we use the `BincodeCodec` to send a stream of `StockEvent`s.
+
+You'll need to install `serde` with the _derive_ feature to follow this example:
+```bash
+$ cargo add -F derive serde
+```
+
+```rust
+use futures::SinkExt;
+use selium::{prelude::*, std::codecs::BincodeCodec};
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Serialize, Deserialize)]
+struct StockEvent {
+ ticker: String,
+ change: f64,
+}
+
+impl StockEvent {
+ pub fn new(ticker: &str, change: f64) -> Self {
+ Self {
+ ticker: ticker.to_owned(),
+ change,
+ }
+ }
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ let connection = selium::custom()
+ .endpoint("127.0.0.1:7001")
+ .with_certificate_authority("certs/client/ca.der")?
+ .with_cert_and_key(
+ "certs/client/localhost.der",
+ "certs/client/localhost.key.der",
+ )?
+ .connect()
+ .await?;
+
+ let mut publisher = connection
+ .publisher("/some/topic")
+ .with_encoder(BincodeCodec::default())
+ .open()
+ .await?;
+
+ publisher.send(StockEvent::new("INTC", -9.0)).await?;
+ publisher.finish().await?;
+
+ Ok(())
+}
+```
+
+## Can I build my own codec?
+
+Yes, and you can use third party codecs like protocol buffers, SBE etc. We'll have a chapter on
+this coming soon.
diff --git a/src/getting_started/first_steps.md b/src/getting_started/first_steps.md
new file mode 100644
index 0000000..0a89e24
--- /dev/null
+++ b/src/getting_started/first_steps.md
@@ -0,0 +1,129 @@
+# First Steps
+
+Selium comprises a client library and a server binary. In order to use Selium, there are 3 basic
+steps:
+1. Create TLS certificates for your client and server
+2. Run the server binary
+3. Integrate the client library into your project
+
+## Step 1 - TLS Certificates
+
+First we need some certs to validate the client and server. Selium uses mutual TLS (_mTLS_) to
+validate both parties cryptographically, making things nice and secure.
+
+We've built a tool to make this easy, so let's install that, then mint our certs:
+
+```bash
+# Install the selium-tools CLI
+$ cargo install selium-tools
+# Use this CLI to create our certificates
+$ selium-tools gen-certs
+```
+
+You should now have a directory in the current path called `certs/`. Inside we have certs for
+the client and server, which you can move to a more convenient location if you like - the paths are
+configurable in code. Both the `client/` and `server/` directories include a copy of the
+certificate authority, which you'll need if you want to create more client certificates later.
+
+## Step 2 - Start the Selium Server
+
+The Selium server allows us to exchange messages between clients. For this example we'll grab a
+copy from [crates.io](https://crates.io/crates/selium-server).
+
+> For production deployments you can also download prebuilt binaries from
+> [GitHub](https://github.com/orgs/seliumlabs/packages?repo_name=selium).
+
+Let's start a new server with our freshly minted certs. In the same directory as your `certs/`
+folder, open a new terminal and run the following commands:
+
+```bash
+# Install Selium server
+$ cargo install selium-server
+# Run the server
+$ selium-server --bind-addr=127.0.0.1:7001
+```
+
+The `selium-server` command will not produce any output by default, but that doesn't mean it
+isn't working! You can increase logging using the verbosity flag:
+
+```bash
+$ selium-server -v # Warnings only
+$ selium-server -vv # Info
+$ selium-server -vvv # Debug
+$ selium-server -vvvv # Trace
+```
+
+## Step 3 - Implement the Selium Client
+
+Selium Client is a composable library API for the Selium Server. Let's have a look at a minimal
+example:
+
+```rust
+use futures::{SinkExt, StreamExt};
+use selium::{prelude::*, std::codecs::StringCodec};
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ let connection = selium::custom() // connect to your own Selium server
+ .endpoint("127.0.0.1:7001") // your Selium server's address
+ .with_certificate_authority("certs/client/ca.der")? // your Selium cert authority
+ .with_cert_and_key(
+ "certs/client/localhost.der",
+ "certs/client/localhost.key.der",
+ )? // your client certificates
+ .connect()
+ .await?;
+
+ let mut publisher = connection
+ .publisher("/some/topic") // choose a topic to group similar messages together
+ .with_encoder(StringCodec) // allows you to exchange string messages between clients
+ .open() // opens a new stream for sending data
+ .await?;
+
+ let mut subscriber = connection
+ .subscriber("/some/topic") // subscribe to the publisher's topic
+ .with_decoder(StringCodec) // use the same codec as the publisher
+ .open() // opens a new stream for receiving data
+ .await?;
+
+ // Send a message and close the publisher
+ publisher.send("Hello, world!".into()).await?;
+ publisher.finish().await?;
+
+ // Receive the message
+ if let Some(Ok(message)) = subscriber.next().await {
+ println!("Received message: {message}");
+ }
+
+ Ok(())
+}
+```
+
+There's a lot to take in here, but for the moment let's just get this baby running!
+
+```bash
+# Create a new Cargo project
+$ cargo new --bin hello-selium
+
+# Move into our project
+$ cd hello-selium
+
+# Add the crate dependencies
+$ cargo add futures
+$ cargo add -F std selium
+$ cargo add -F macros,rt tokio
+```
+
+Now copy and paste the code above into `hello-selium/src/main.rs`.
+
+Now let's execute the project and start exchanging some messages! Make sure your server is still
+running from the previous step.
+
+```bash
+$ cargo run
+Received message: Hello, world!
+```
+
+## Next Steps
+
+We've just setup a working Selium publish/subscribe project, but [you can also use RPC too.](./request_reply.md)
diff --git a/src/getting_started/request_reply.md b/src/getting_started/request_reply.md
new file mode 100644
index 0000000..1a81bdc
--- /dev/null
+++ b/src/getting_started/request_reply.md
@@ -0,0 +1,90 @@
+# Request/Reply (RPC pattern)
+
+In the previous chapter we discussed our first steps with the publish/subscribe pattern. In this
+chapter we will explore Selium's other major communication pattern - request/reply (otherwise known
+as RPC).
+
+Request/reply is useful in the same way that a REST API is. For instance, you can use Selium to
+replace your internal APIs:
+
+```rust
+#[derive(Deserialize)]
+enum Request {
+ LookupCustomer(u64),
+ // Other requests
+}
+
+async fn main() -> Result<(), Box> {
+ let connection = selium::custom()
+ ...
+
+ // Setup a database connection to demonstrate state sharing
+ let mut db = create_db()?;
+
+ let mut replier = connection
+ .replier("/customers/api") // choose a topic to serve your API on
+ .with_request_decoder(StringCodec) // let's use a StringCodec to interface with JSON clients
+ .with_request_encoder(StringCodec) // let's use a StringCodec to interface with JSON clients
+ .with_handler(move |json| {
+ let req: Request = serde_json::from_str(&json)?;
+ match req {
+ Request::LookupCustomer(id) => lookup_customer(&mut db, r.client_id),
+ }
+ })
+ .open()
+ .await?;
+
+ replier.listen().await?;
+
+ Ok(())
+}
+
+async fn lookup_customer(db: &mut Database, id: u64) -> Result {
+ // Lookup request...
+
+ // Reply with a JSON response
+ Ok(r#"
+ {
+ "name": "John Doe",
+ "age": 43,
+ "phones": [
+ "+44 1234567",
+ "+44 2345678"
+ ]
+ }"#)
+}
+```
+
+And now for the requestor:
+
+```rust
+async fn main() -> Result<(), Box> {
+ let connection = selium::custom()
+ ...
+
+ let requestor = connection
+ .requestor("/some/endpoint")
+ .with_request_encoder(StringCodec)
+ .with_reply_decoder(StringCodec)
+ .with_request_timeout(Duration::from_secs(1))?
+ .open()
+ .await?;
+
+ let customer = requestor.request(r#"{"LookupCustomer": 85028}"#).await.unwrap();
+
+ Ok(())
+}
+```
+
+### Benefits over HTTP
+
+1. No web servers
+2. Built in mutual TLS
+3. Ability to exchange pure Rust types over the wire
+4. Transport much larger amounts of data, much faster
+5. Multiple messaging patterns at your fingertips
+6. 100% Rust toolchain
+
+## Next Steps
+
+Nice work, we've got all kinds of data flowing! Now it's time to learn about [reusing Selium connections.](./reusing_connections.md)
diff --git a/src/getting_started/reusing_connections.md b/src/getting_started/reusing_connections.md
new file mode 100644
index 0000000..fa2670c
--- /dev/null
+++ b/src/getting_started/reusing_connections.md
@@ -0,0 +1,37 @@
+# Reusing Connections
+
+Selium supports reusing connections to the Selium server, otherwise known as _multiplexing_. This
+allows you to maintain a single connection that publishes and/or subscribes to multiple topics
+simultaneously.
+
+Let's have another look at that code from the previous chapter:
+
+```rust
+async fn main() -> Result<(), Box> {
+ let connection = selium::custom()
+ ...
+
+ let mut publisher = connection
+ .publisher("/some/topic") // choose a topic to group similar messages together
+ .with_encoder(StringCodec) // allows you to exchange string messages between clients
+ .open() // opens a new stream for sending data
+ .await?;
+
+ let mut subscriber = connection
+ .subscriber("/some/topic") // subscribe to the publisher's topic
+ .with_decoder(StringCodec) // use the same codec as the publisher
+ .open() // opens a new stream for receiving data
+ .await?;
+
+ ...
+
+ Ok(())
+}
+```
+
+We can see that each time a new behaviour is required, we create a new stream using
+`Client::publisher` and `Client::subscriber`. For each new stream, the full range of builder
+options is available, uniquely for that stream.
+
+Next, let's see how we can send/receive data more ergonomically with
+[`Stream` and `Sink` traits](./streams_and_sinks.md).
diff --git a/src/getting_started/streams_and_sinks.md b/src/getting_started/streams_and_sinks.md
new file mode 100644
index 0000000..b2fb8a1
--- /dev/null
+++ b/src/getting_started/streams_and_sinks.md
@@ -0,0 +1,75 @@
+# `Stream` and `Sink` traits
+
+If you've ever used Rust's `futures` or `tokio` crates, you'll almost certainly be aware
+of the [`Stream`](https://docs.rs/futures/latest/futures/stream/trait.Stream.html) and
+[`Sink`](https://docs.rs/futures/latest/futures/sink/trait.Sink.html) traits. These
+traits govern the sending and receiving of messages between components, and are arguably
+a de facto standard in Rust.
+
+> If you've never used these traits before, don't worry! Not only are they quite
+> intuitive, but the [`futures` crate](https://docs.rs/futures/latest/futures/index.html)
+> has great documentation explaining how they work.
+
+The good news for Selium users, is that Selium's `Publisher` and `Subscriber` both
+implement `Sink` and `Stream` respectively. Thus they should fit hand in glove with your
+existing streaming applications.
+
+You can also make use of the `futures` crate's
+[`StreamExt`](https://docs.rs/futures/latest/futures/stream/trait.StreamExt.html) and
+[`SinkExt`](https://docs.rs/futures/latest/futures/sink/trait.SinkExt.html) extensions.
+These traits are implemented by default on any `Stream`/`Sink` implementations (like
+Selium's) and contain lots of helpful tools.
+
+Let's go back to our example from previous chapters and see what we can do with `SinkExt`
+on a `Publisher`:
+
+```rust
+use futures::{future, stream, SinkExt, Stream, StreamExt};
+use selium::{prelude::*, std::codecs::StringCodec};
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ let connection = selium::custom()
+ .endpoint("127.0.0.1:7001")
+ .with_certificate_authority("certs/client/ca.der")? // your Selium cert authority
+ .with_cert_and_key(
+ "certs/client/localhost.der",
+ "certs/client/localhost.key.der",
+ )? // your client certificates
+ .connect() // your Selium server's address
+ .await?;
+
+ let publisher = connection
+ .publisher("/some/topic") // choose a topic to group similar messages together
+ .with_encoder(StringCodec) // allows you to exchange string messages between clients
+ .open() // opens a new stream for sending data
+ .await?;
+
+ let mut sink = publisher.with(|item: String| {
+ if item.contains('@') {
+ future::ok(item)
+ } else {
+ future::ok("default_email@company.org".into())
+ }
+ });
+ sink.send_all(&mut some_stream()).await?;
+
+ Ok(())
+}
+
+fn some_stream() -> impl Stream>> {
+ stream::iter(vec![
+ "hello@world.com".into(),
+ "some@example.net".into(),
+ "notanemail.com".into(),
+ ])
+ .map(|email| Ok(email))
+}
+```
+
+In this example, we take a stream of email addresses and optionally map invalid email addresses to
+a default email address. Of course, this is not a very useful example, but the Rust ecosystem is
+_full_ of `Stream`s and `Sink`s. Selium's native support for these traits allows you to slot Selium
+client into your libraries and applications with ease.
+
+Next, let's get to grips with [codecs](./codecs.md).
diff --git a/src/introduction.md b/src/introduction.md
new file mode 100644
index 0000000..1df949e
--- /dev/null
+++ b/src/introduction.md
@@ -0,0 +1,39 @@
+# Welcome to Selium's user guide
+
+Ahoy, and thanks for being part of the community! In this wiki you'll learn about Selium, its
+components and how to use them.
+
+If at any point you find yourself thinking "I wish they'd cover ...", please
+[start a new discussion](https://github.com/seliumlabs/selium/discussions/new?category=ideas). We would really value
+your feedback and ideas. Thanks!
+
+## What is Selium?
+
+You may have seen this description of Selium on the organisation readme: "Selium is an extremely
+developer friendly composable messaging platform with zero build time configuration." This is a
+nice statement, but lacks any depth. So, let's add some, shall we?
+
+**_"Selium is a messaging platform."_** Messaging is simply a method of exchanging data in a
+structured way. You may have come across the term "Pub-Sub", which is a method of sending
+(_publishing_) the same message to lots of receivers (_subscribers_). That may be a bit of a grey
+area to people new to messaging, but you'll definitely know this one: "HTTP". Yep that's right, web
+requests are a form of messaging too. It's called an "RPC", or more colloquially "request-reply".
+
+**_"Selium is composable."_** We've built Selium as a collection of parts that you can stick
+together to aggregate, manipulate and disseminate your data however you require, much like
+everyone's favourite childrens' toy that rhymes with "Pego". You guessed it - Nanoblocks!
+
+**_"Selium is extremely developer friendly."_** Selium is designed from the ground up for developer
+ergonomics. Whatever requirements you have for your services and data, you can compose them in at
+runtime using our functional API. In other words, Selium has _"zero build time configuration"_.
+
+## Who should use Selium?
+
+_(Anyone with ops-related trauma)_
+
+Selium is designed for software developers. If your project needs to move data around, expose
+services, discover services, introduce resiliency, scale out, speed up, produce or consume events,
+or otherwise run screaming from HTTP, Selium is for you. Liberate your stack from DevOps, simplify
+your deployment pipeline and never use the word "idempotent" again.
+
+Let's [get started!](./getting_started/first_steps.md)
diff --git a/src/selium.png b/src/selium.png
new file mode 100644
index 0000000..423ca3f
Binary files /dev/null and b/src/selium.png differ
diff --git a/theme/css/chrome.css b/theme/css/chrome.css
new file mode 100644
index 0000000..55cb62e
--- /dev/null
+++ b/theme/css/chrome.css
@@ -0,0 +1,649 @@
+/* CSS for UI elements (a.k.a. chrome) */
+
+@import "variables.css";
+
+html {
+ scrollbar-color: var(--scrollbar) var(--bg);
+}
+#searchresults a,
+.content a:link,
+a:visited,
+a > .hljs {
+ color: var(--links);
+}
+
+/*
+ body-container is necessary because mobile browsers don't seem to like
+ overflow-x on the body tag when there is a tag.
+*/
+#body-container {
+ /*
+ This is used when the sidebar pushes the body content off the side of
+ the screen on small screens. Without it, dragging on mobile Safari
+ will want to reposition the viewport in a weird way.
+ */
+ overflow-x: clip;
+}
+
+/* Menu Bar */
+
+#menu-bar,
+#menu-bar-hover-placeholder {
+ z-index: 101;
+ margin: auto calc(0px - var(--page-padding));
+}
+#menu-bar {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ background-color: var(--bg);
+ border-block-end-color: var(--bg);
+ border-block-end-width: 1px;
+ border-block-end-style: solid;
+}
+#menu-bar.sticky,
+.js #menu-bar-hover-placeholder:hover + #menu-bar,
+.js #menu-bar:hover,
+.js.sidebar-visible #menu-bar {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0 !important;
+}
+#menu-bar-hover-placeholder {
+ position: sticky;
+ position: -webkit-sticky;
+ top: 0;
+ height: var(--menu-bar-height);
+}
+#menu-bar.bordered {
+ border-block-end-color: var(--table-border-color);
+}
+#menu-bar i,
+#menu-bar .icon-button {
+ position: relative;
+ padding: 0 8px;
+ z-index: 10;
+ line-height: var(--menu-bar-height);
+ cursor: pointer;
+ transition: color 0.5s;
+}
+@media only screen and (max-width: 420px) {
+ #menu-bar i,
+ #menu-bar .icon-button {
+ padding: 0 5px;
+ }
+}
+
+.icon-button {
+ border: none;
+ background: none;
+ padding: 0;
+ color: inherit;
+}
+.icon-button i {
+ margin: 0;
+}
+
+.right-buttons {
+ margin: 0 15px;
+}
+.right-buttons a {
+ text-decoration: none;
+}
+
+.left-buttons {
+ display: flex;
+ margin: 0 5px;
+}
+.no-js .left-buttons button {
+ display: none;
+}
+
+.menu-title {
+ display: inline-block;
+ font-weight: 200;
+ font-size: 2.4rem;
+ line-height: var(--menu-bar-height);
+ text-align: center;
+ margin: 0;
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.js .menu-title {
+ cursor: pointer;
+}
+
+.menu-bar,
+.menu-bar:visited,
+.nav-chapters,
+.nav-chapters:visited,
+.mobile-nav-chapters,
+.mobile-nav-chapters:visited,
+.menu-bar .icon-button,
+.menu-bar a i {
+ color: var(--icons);
+}
+
+.menu-bar i:hover,
+.menu-bar .icon-button:hover,
+.nav-chapters:hover,
+.mobile-nav-chapters i:hover {
+ color: var(--icons-hover);
+}
+
+/* Nav Icons */
+
+.nav-chapters {
+ font-size: 2.5em;
+ text-align: center;
+ text-decoration: none;
+
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ margin: 0;
+ max-width: 150px;
+ min-width: 90px;
+
+ display: flex;
+ justify-content: center;
+ align-content: center;
+ flex-direction: column;
+
+ transition:
+ color 0.5s,
+ background-color 0.5s;
+}
+
+.nav-chapters:hover {
+ text-decoration: none;
+ background-color: var(--theme-hover);
+ transition:
+ background-color 0.15s,
+ color 0.15s;
+}
+
+.nav-wrapper {
+ margin-block-start: 50px;
+ display: none;
+}
+
+.mobile-nav-chapters {
+ font-size: 2.5em;
+ text-align: center;
+ text-decoration: none;
+ width: 90px;
+ border-radius: 5px;
+ background-color: var(--sidebar-bg);
+}
+
+/* Only Firefox supports flow-relative values */
+.previous {
+ float: left;
+}
+[dir="rtl"] .previous {
+ float: right;
+}
+
+/* Only Firefox supports flow-relative values */
+.next {
+ float: right;
+ right: var(--page-padding);
+}
+[dir="rtl"] .next {
+ float: left;
+ right: unset;
+ left: var(--page-padding);
+}
+
+/* Use the correct buttons for RTL layouts*/
+[dir="rtl"] .previous i.fa-angle-left:before {
+ content: "\f105";
+}
+[dir="rtl"] .next i.fa-angle-right:before {
+ content: "\f104";
+}
+
+@media only screen and (max-width: 1080px) {
+ .nav-wide-wrapper {
+ display: none;
+ }
+ .nav-wrapper {
+ display: block;
+ }
+}
+
+/* sidebar-visible */
+@media only screen and (max-width: 1380px) {
+ #sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wide-wrapper {
+ display: none;
+ }
+ #sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wrapper {
+ display: block;
+ }
+}
+
+/* Inline code */
+
+:not(pre) > .hljs {
+ display: inline;
+ padding: 0.1em 0.3em;
+ border-radius: 3px;
+}
+
+:not(pre):not(a) > .hljs {
+ color: var(--inline-code-color);
+ overflow-x: initial;
+}
+
+a:hover > .hljs {
+ text-decoration: underline;
+}
+
+pre {
+ position: relative;
+}
+pre > .buttons {
+ position: absolute;
+ z-index: 100;
+ right: 0px;
+ top: 2px;
+ margin: 0px;
+ padding: 2px 0px;
+
+ color: var(--sidebar-fg);
+ cursor: pointer;
+ visibility: hidden;
+ opacity: 0;
+ transition:
+ visibility 0.1s linear,
+ opacity 0.1s linear;
+}
+pre:hover > .buttons {
+ visibility: visible;
+ opacity: 1;
+}
+pre > .buttons :hover {
+ color: var(--sidebar-active);
+ border-color: var(--icons-hover);
+ background-color: var(--theme-hover);
+}
+pre > .buttons i {
+ margin-inline-start: 8px;
+}
+pre > .buttons button {
+ cursor: inherit;
+ margin: 0px 5px;
+ padding: 3px 5px;
+ font-size: 14px;
+
+ border-style: solid;
+ border-width: 1px;
+ border-radius: 4px;
+ border-color: var(--icons);
+ background-color: var(--theme-popup-bg);
+ transition: 100ms;
+ transition-property: color, border-color, background-color;
+ color: var(--icons);
+}
+@media (pointer: coarse) {
+ pre > .buttons button {
+ /* On mobile, make it easier to tap buttons. */
+ padding: 0.3rem 1rem;
+ }
+
+ .sidebar-resize-indicator {
+ /* Hide resize indicator on devices with limited accuracy */
+ display: none;
+ }
+}
+pre > code {
+ display: block;
+ padding: 1rem;
+}
+
+/* FIXME: ACE editors overlap their buttons because ACE does absolute
+ positioning within the code block which breaks padding. The only solution I
+ can think of is to move the padding to the outer pre tag (or insert a div
+ wrapper), but that would require fixing a whole bunch of CSS rules.
+*/
+.hljs.ace_editor {
+ padding: 0rem 0rem;
+}
+
+pre > .result {
+ margin-block-start: 10px;
+}
+
+/* Search */
+
+#searchresults a {
+ text-decoration: none;
+}
+
+mark {
+ border-radius: 2px;
+ padding-block-start: 0;
+ padding-block-end: 1px;
+ padding-inline-start: 3px;
+ padding-inline-end: 3px;
+ margin-block-start: 0;
+ margin-block-end: -1px;
+ margin-inline-start: -3px;
+ margin-inline-end: -3px;
+ background-color: var(--search-mark-bg);
+ transition: background-color 300ms linear;
+ cursor: pointer;
+}
+
+mark.fade-out {
+ background-color: rgba(0, 0, 0, 0) !important;
+ cursor: auto;
+}
+
+.searchbar-outer {
+ margin-inline-start: auto;
+ margin-inline-end: auto;
+ max-width: var(--content-max-width);
+}
+
+#searchbar {
+ width: 100%;
+ margin-block-start: 5px;
+ margin-block-end: 0;
+ margin-inline-start: auto;
+ margin-inline-end: auto;
+ padding: 10px 16px;
+ transition: box-shadow 300ms ease-in-out;
+ border: 1px solid var(--searchbar-border-color);
+ border-radius: 3px;
+ background-color: var(--searchbar-bg);
+ color: var(--searchbar-fg);
+}
+#searchbar:focus,
+#searchbar.active {
+ box-shadow: 0 0 3px var(--searchbar-shadow-color);
+}
+
+.searchresults-header {
+ font-weight: bold;
+ font-size: 1em;
+ padding-block-start: 18px;
+ padding-block-end: 0;
+ padding-inline-start: 5px;
+ padding-inline-end: 0;
+ color: var(--searchresults-header-fg);
+}
+
+.searchresults-outer {
+ margin-inline-start: auto;
+ margin-inline-end: auto;
+ max-width: var(--content-max-width);
+ border-block-end: 1px dashed var(--searchresults-border-color);
+}
+
+ul#searchresults {
+ list-style: none;
+ padding-inline-start: 20px;
+}
+ul#searchresults li {
+ margin: 10px 0px;
+ padding: 2px;
+ border-radius: 2px;
+}
+ul#searchresults li.focus {
+ background-color: var(--searchresults-li-bg);
+}
+ul#searchresults span.teaser {
+ display: block;
+ clear: both;
+ margin-block-start: 5px;
+ margin-block-end: 0;
+ margin-inline-start: 20px;
+ margin-inline-end: 0;
+ font-size: 0.8em;
+}
+ul#searchresults span.teaser em {
+ font-weight: bold;
+ font-style: normal;
+}
+
+/* Sidebar */
+
+.sidebar {
+ position: fixed;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: var(--sidebar-width);
+ font-size: 0.875em;
+ box-sizing: border-box;
+ -webkit-overflow-scrolling: touch;
+ overscroll-behavior-y: contain;
+ background-color: var(--sidebar-bg);
+ color: var(--sidebar-fg);
+}
+[dir="rtl"] .sidebar {
+ left: unset;
+ right: 0;
+}
+.sidebar-resizing {
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.no-js .sidebar,
+.js:not(.sidebar-resizing) .sidebar {
+ transition: transform 0.3s; /* Animation: slide away */
+}
+.sidebar code {
+ line-height: 2em;
+}
+.sidebar .sidebar-scrollbox {
+ overflow-y: auto;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 10px 10px;
+}
+.sidebar .sidebar-resize-handle {
+ position: absolute;
+ cursor: col-resize;
+ width: 0;
+ right: calc(var(--sidebar-resize-indicator-width) * -1);
+ top: 0;
+ bottom: 0;
+ display: flex;
+ align-items: center;
+}
+
+.sidebar-resize-handle .sidebar-resize-indicator {
+ width: 100%;
+ height: 12px;
+ background-color: var(--icons);
+ margin-inline-start: var(--sidebar-resize-indicator-space);
+}
+
+[dir="rtl"] .sidebar .sidebar-resize-handle {
+ left: calc(var(--sidebar-resize-indicator-width) * -1);
+ right: unset;
+}
+.js .sidebar .sidebar-resize-handle {
+ cursor: col-resize;
+ width: calc(var(--sidebar-resize-indicator-width) - var(--sidebar-resize-indicator-space));
+}
+/* sidebar-hidden */
+#sidebar-toggle-anchor:not(:checked) ~ .sidebar {
+ transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
+ z-index: -1;
+}
+[dir="rtl"] #sidebar-toggle-anchor:not(:checked) ~ .sidebar {
+ transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
+}
+.sidebar::-webkit-scrollbar {
+ background: var(--sidebar-bg);
+}
+.sidebar::-webkit-scrollbar-thumb {
+ background: var(--scrollbar);
+}
+
+.sidebar .sidebar-scrollbox .sidebar-book-logo a img {
+ display: block;
+ margin-bottom: 2em;
+ margin-left: auto;
+ margin-right: auto;
+ width: 70%;
+ max-width: max-content;
+}
+
+/* sidebar-visible */
+#sidebar-toggle-anchor:checked ~ .page-wrapper {
+ transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
+}
+[dir="rtl"] #sidebar-toggle-anchor:checked ~ .page-wrapper {
+ transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
+}
+@media only screen and (min-width: 620px) {
+ #sidebar-toggle-anchor:checked ~ .page-wrapper {
+ transform: none;
+ margin-inline-start: calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width));
+ }
+ [dir="rtl"] #sidebar-toggle-anchor:checked ~ .page-wrapper {
+ transform: none;
+ }
+}
+
+.chapter {
+ list-style: none outside none;
+ padding-inline-start: 0;
+ line-height: 2.2em;
+}
+
+.chapter ol {
+ width: 100%;
+}
+
+.chapter li {
+ display: flex;
+ color: var(--sidebar-non-existant);
+}
+.chapter li a {
+ display: block;
+ padding: 0;
+ text-decoration: none;
+ color: var(--sidebar-fg);
+}
+
+.chapter li a:hover {
+ color: var(--sidebar-active);
+}
+
+.chapter li a.active {
+ color: var(--sidebar-active);
+}
+
+.chapter li > a.toggle {
+ cursor: pointer;
+ display: block;
+ margin-inline-start: auto;
+ padding: 0 10px;
+ user-select: none;
+ opacity: 0.68;
+}
+
+.chapter li > a.toggle div {
+ transition: transform 0.5s;
+}
+
+/* collapse the section */
+.chapter li:not(.expanded) + li > ol {
+ display: none;
+}
+
+.chapter li.chapter-item {
+ line-height: 1.5em;
+ margin-block-start: 0.6em;
+}
+
+.chapter li.expanded > a.toggle div {
+ transform: rotate(90deg);
+}
+
+.spacer {
+ width: 100%;
+ height: 3px;
+ margin: 5px 0px;
+}
+.chapter .spacer {
+ background-color: var(--sidebar-spacer);
+}
+
+@media (-moz-touch-enabled: 1), (pointer: coarse) {
+ .chapter li a {
+ padding: 5px 0;
+ }
+ .spacer {
+ margin: 10px 0;
+ }
+}
+
+.section {
+ list-style: none outside none;
+ padding-inline-start: 20px;
+ line-height: 1.9em;
+}
+
+/* Theme Menu Popup */
+
+.theme-popup {
+ position: absolute;
+ left: 10px;
+ top: var(--menu-bar-height);
+ z-index: 1000;
+ border-radius: 4px;
+ font-size: 0.7em;
+ color: var(--fg);
+ background: var(--theme-popup-bg);
+ border: 1px solid var(--theme-popup-border);
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ display: none;
+ /* Don't let the children's background extend past the rounded corners. */
+ overflow: hidden;
+}
+[dir="rtl"] .theme-popup {
+ left: unset;
+ right: 10px;
+}
+.theme-popup .default {
+ color: var(--icons);
+}
+.theme-popup .theme {
+ width: 100%;
+ border: 0;
+ margin: 0;
+ padding: 2px 20px;
+ line-height: 25px;
+ white-space: nowrap;
+ text-align: start;
+ cursor: pointer;
+ color: inherit;
+ background: inherit;
+ font-size: inherit;
+}
+.theme-popup .theme:hover {
+ background-color: var(--theme-hover);
+}
+
+.theme-selected::before {
+ display: inline-block;
+ content: "✓";
+ margin-inline-start: -14px;
+ width: 14px;
+}
diff --git a/theme/favicon.png b/theme/favicon.png
new file mode 100644
index 0000000..5b874f5
Binary files /dev/null and b/theme/favicon.png differ
diff --git a/theme/head.hbs b/theme/head.hbs
new file mode 100644
index 0000000..bc30e59
--- /dev/null
+++ b/theme/head.hbs
@@ -0,0 +1,9 @@
+
+
+
diff --git a/theme/index.hbs b/theme/index.hbs
new file mode 100644
index 0000000..7ee5b49
--- /dev/null
+++ b/theme/index.hbs
@@ -0,0 +1,349 @@
+
+
+
+
+
+ {{ title }}
+ {{#if is_print }}
+
+ {{/if}}
+ {{#if base_url}}
+
+ {{/if}}
+
+
+
+ {{> head}}
+
+
+
+
+
+ {{#if favicon_svg}}
+
+ {{/if}}
+ {{#if favicon_png}}
+
+ {{/if}}
+
+
+
+ {{#if print_enable}}
+
+ {{/if}}
+
+
+
+ {{#if copy_fonts}}
+
+ {{/if}}
+
+
+
+
+
+
+
+ {{#each additional_css}}
+
+ {{/each}}
+
+ {{#if mathjax_support}}
+
+
+ {{/if}}
+
+
+