Compare commits

..

46 Commits

Author SHA1 Message Date
42698372ad Split home page into two columns 2021-06-16 07:28:38 +00:00
eddde95b57 Adjust margins 2021-05-30 05:22:33 +00:00
b5b9e68df0 Remove darkmode selector 2021-05-30 05:08:37 +00:00
5bcad7eb77 Fix duplicate sidebar / topbar problem 2021-05-30 05:00:35 +00:00
2be883b19f Set home photo default sizes 2021-05-26 07:09:00 +00:00
98f61d6d2f Remove "logo" and "icon" from alt text 2021-05-26 07:00:11 +00:00
4ebf756c88 Convert project links back to headers from paragraphs 2021-05-26 06:59:01 +00:00
0bea1250bc Convert project links from headers to paragraphs 2021-05-16 04:05:48 +00:00
175c9e77c9 Improve alt texts 2021-04-28 20:08:59 +00:00
64a5cdd775 Add links to software projects 2021-04-28 08:49:34 +00:00
9ee0254509 Add pirate favicon 2021-04-27 01:14:31 +00:00
fc2afda5d0 Remove underline from headers 2021-04-27 01:14:09 +00:00
63842070cc Edit Bypassing Ports 2021-04-27 01:13:18 +00:00
1a98f7a163 Add bypassing ports article 2021-04-11 07:13:30 +00:00
5b0f01d804 Fix things 2021-04-11 07:13:20 +00:00
38607ec437 Add margin notes 2021-04-09 05:52:30 +00:00
8fb1b29aef Add My Backup Strategy article 2021-04-09 05:18:18 +00:00
21c25413fc Support setting an article as "wide" 2021-04-09 05:17:51 +00:00
3e0e0beb0f Add styling for <code> tags 2021-04-09 05:01:11 +00:00
fac9de10aa Add copyright 2021-04-09 01:11:55 +00:00
e9c31546ab Reduce text contrast
eee -> ddd
fff -> eee
2021-04-09 00:44:39 +00:00
be19efb887 Make the article pages minimal 2021-04-09 00:36:39 +00:00
43b28bc982 Update email 2021-04-08 23:50:08 +00:00
01e83795ad Switch to better solar car photo 2020-11-21 01:25:18 +00:00
bfd988a53e Fix LED Dress video html centering 2020-11-21 01:24:56 +00:00
786418496b Move to custom dark mode 2020-11-02 05:05:25 +00:00
6bed384805 Disable rendering useless pages 2020-11-02 03:35:54 +00:00
6d2247b210 Cope pelicanconf to publishconf 2020-11-02 02:43:40 +00:00
dd44ff4c6d Add linux flavour article 2020-11-02 02:43:21 +00:00
32d76be296 Inline fonts, fix links, add instant page 2020-11-02 02:42:28 +00:00
3dc90a272d Improve darkmode button contrast, fix fonts 2020-09-04 00:46:03 +00:00
809806c8cd Add meta tags and optimize loading 2020-09-04 00:40:33 +00:00
975484a03c Add location to intro 2020-09-03 23:51:34 +00:00
5a91907f4c Invert contact icons in darkmode 2020-08-29 00:18:12 +00:00
f2e94e2ae9 Replace signature logo with photo and name 2020-08-27 23:33:03 +00:00
35fce33baa Add contact icons 2020-08-27 20:30:43 +00:00
b8f02d92da Add darkmode 2020-06-09 19:40:38 +00:00
5abcd6f672 Add resume to home page 2020-06-03 04:26:52 +00:00
9bc09ceebc Remove navigation links for now 2020-05-30 01:43:06 +00:00
e91c976ddb Move all projects from old website over 2020-05-30 01:40:22 +00:00
ba35bf5466 Transform hard-coded theme into Jinja template 2020-05-10 01:36:58 +00:00
5190e4b812 Finalize theme 2020-05-09 22:17:01 +00:00
7a4b8385dc Begin a basic responsive theme 2020-05-08 00:01:50 +00:00
13d3d4842f Scrap everything, switch to Pelican instead 2020-05-06 04:25:36 +00:00
92078664c0 Adjust grid css 2020-05-05 06:14:26 +00:00
e5aa80e123 Remove footers, install font 2018-08-03 04:14:36 +00:00
72 changed files with 1884 additions and 8195 deletions

124
.gitignore vendored
View File

@@ -1,16 +1,114 @@
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
coverage
node_modules
build
.env.local
.env.development.local
.env.test.local
.env.production.local
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
# Editor
*.swp
*.swo
# DB
db.sqlite3
# Test mount
test/
# VS Code
.vscode/
output/

View File

@@ -1 +0,0 @@
My personal website.

343
content/backup-strategy.md Normal file
View File

@@ -0,0 +1,343 @@
Title: My Backup Strategy
Date: 2021-04-08
Category: Writing
Summary: Details about the backup system for all of my data.
Wide: true
[TOC]
Regularly backing up all the data I care about is very important to me. This
article outlines my strategy to make sure I never lose essential data.
## Motivation
Backups should be as automatic as possible. This ensures laziness and
forgetfulness won't interfere with the regularity.
All software used to create and store the backups should be free and open source
so I'm not depending on the survival of a company.
Backups need to be tested to ensure they are correct and happening regularly.
Multiple copies of the backups should exist, including at least one offsite to
protect against my building burning down.
Backups should also be incremental when possible (rather than mirror copies) so
an accidental deletion isn't propagated into the backups, making the file
irrecoverable.
## Strategy
I have one backup folder `/mnt/backup` on my media server at home that serves as
the destination for all my backup sources. All scheduled automatic backups write
to their own subfolder inside of it.
This backup folder is then synced to encrypted 2.5" 1 TB hard drives which I
rotate between my bag, offsite, and my parents' house.
## Backup Sources
I use the tool `rdiff-backup` extensively because it allows me to take
incremental backups locally or over SSH. It acts very similar to `rsync` and has
no configuration.
### Email
I have every email since 2010 backed up continuously in case my email provider
disappears.
I use `offlineimap` to sync my mail to the directory `~/email` on my media
server as a Maildir. Since offlineimap is only a syncing tool, the emails need
to be copied elsewhere to be backed up. I run `rdiff-backup` from a weekly cron
job:
<span class="aside">I'll explain what backup_check.txt does below</span>
```
*/15 * * * * offlineimap > /var/log/offlineimap.log 2>&1
00 12 * * 1 date -Iseconds > /home/email/email/backup_check.txt
20 12 * * 1 rdiff-backup /home/email/email /mnt/backup/local/email/
40 12 * * 1 rdiff-backup --remove-older-than 12B --force /mnt/backup/local/email/
```
Here's my `.offlineimaprc` for reference:
```
[general]
accounts = main
[Account main]
localrepository = Local
remoterepository = Remote
[Repository Local]
type = Maildir
localfolders = ~/email
[Repository Remote]
type = IMAP
readonly = True
folderfilter = lambda foldername: foldername not in ['Trash', 'Spam', 'Drafts']
remotehost = example.com
remoteuser = mail@example.com
remotepass = supersecret
sslcacertfile = /etc/ssl/certs/ca-certificates.crt
```
### Notes
I use Standard Notes to take notes and wrote the tool
[standardnotes-fs](https://github.com/tannercollin/standardnotes-fs) to mount my
notes as a file system to view and edit them as plain text files.
I take weekly backups of the mounted file system on my media server with cron:
```
00 12 * * 1 date -Iseconds > /home/notes/notes/backup_check.txt
15 12 * * 1 rdiff-backup /home/notes/notes /mnt/backup/local/notes/
```
### Nextcloud
I self-host a Nextcloud instance to store all my personal documents (non-code
projects, tax forms, spreadsheets, etc.). Since it's only a syncing software,
the files need to be copied elsewhere to be backed up.
I take weekly backups of the Nextcloud data folder with cron:
```
00 12 * * 1 rdiff-backup /var/www/nextcloud/data/tanner/files /mnt/backup/local/nextcloud/
30 12 * * 1 rdiff-backup --remove-older-than 12B --force /mnt/backup/local/nextcloud/
```
### Gitea
I self-host a Gitea instance to store all my git repositories for code-based
projects. My home folder is also a git repo so I can easily sync my config files
and password database between servers and machines.
I take weekly backups of the Gitea data folder with cron:
```
00 12 * * 1 date -Iseconds > /home/gitea/gitea/data/backup_check.txt
10 12 * * 1 rdiff-backup --exclude **data/indexers --exclude **data/sessions /home/gitea/gitea/data /mnt/backup/local/gitea/
35 12 * * 1 rdiff-backup --remove-older-than 12B --force /mnt/backup/local/gitea/
```
### Telegram
Telegram Messenger is my main app for communication. My parents, most of my
friends, and friend groups are on there so I don't want to lose those messages
in case Telegram disappears or my account gets banned.
<span class="aside">Saves the messages to a sqlite db</span>
Telegram includes a data export feature, but it can't be automated. Instead I
run the deprecated software
[telegram-export](https://github.com/expectocode/telegram-export) hourly with
cron:
```
0 * * * * bash -c 'timeout 50m /home/tanner/opt/telegram-export/env/bin/python -m telegram_export' > /var/log/telegramexport.log 2>&1
```
It likes to hang, so `timeout` kills it if it's still running after 50 minutes.
Hasn't corrupted the database yet.
### Phone
[Signal
Messenger](https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms&hl=en_CA&gl=US)
automatically exports a copy of my text messages database, and
[Aegis](https://play.google.com/store/apps/details?id=com.beemdevelopment.aegis&hl=en_CA&gl=US)
allows me to export an encrypted JSON file of my two-factor authentication
codes.
I mount my phone's internal storage as a file system on my desktop using
[adbfs-rootless](https://github.com/spion/adbfs-rootless). I then rsync the
files over to my media server:
```
$ ./adbfs ~/mntphone
$ time rsync -Wav \
--exclude '*cache' --exclude nobackup \
--exclude '*thumb*' --exclude 'Telegram *' \
--exclude 'collection.media' \
--exclude 'org.thunderdog.challegram' \
--exclude '.trashed-*' --exclude '.pending-*' \
~/mntphone/storage/emulated/0/ \
localmediaserver:/mnt/backup/files/phone/
```
Unfortunately this is a manual process because I need to plug my phone in each
time. Ideally it would happen automatically while I'm asleep and the phone is
charging.
### Miscellaneous Files
The directory `/backup/files` is a repository for any kind of files I want to
keep forever. My phone data, old archives, computer files, Minecraft worlds,
files from previous jobs, and so on.
All the files will be included in the 1 TB hard drive backup rotations.
### Web Services
Web services that I run like [txt.t0.vc](https://txt.t0.vc) and
[QotNews](https://news.t0.vc) are backed up daily, weekly, and monthly depending
on how frequently the data changes.
I run `rdiff-backup` on the remote server with cron:
```
00 14 * * * date -Iseconds > /home/tanner/tbot/t0txt/data/backup_check.txt
04 14 * * * rdiff-backup /home/tanner/tbot/t0txt/data tbotbak@remotebackup::/mnt/backup/remote/tbotbak/daily/t0txt/
14 14 * * * rdiff-backup --remove-older-than 12B --force tbotbak@remotebackup::/mnt/backup/remote/tbotbak/daily/t0txt/
24 14 * * 1 rdiff-backup /home/tanner/tbot/t0txt/data tbotbak@remotebackup::/mnt/backup/remote/tbotbak/weekly/t0txt/
34 14 * * 1 rdiff-backup --remove-older-than 12B --force tbotbak@remotebackup::/mnt/backup/remote/tbotbak/weekly/t0txt/
44 14 1 * * rdiff-backup /home/tanner/tbot/t0txt/data tbotbak@remotebackup::/mnt/backup/remote/tbotbak/monthly/t0txt/
55 14 1 * * rdiff-backup --remove-older-than 12B --force tbotbak@remotebackup::/mnt/backup/remote/tbotbak/monthly/t0txt/
```
The `tbotbak` user has write access to the `/mnt/backup/remote/tbotbak`
directory only. It has its own passwordless SSH key that's only permitted to run
the `rdiff-backup --server` command for security.
### Protospace
I run a lot of services for [Protospace](https://protospace.ca/), my city's
makerspace.
The member portal I wrote called [Spaceport](https://my.protospace.ca/) creates
an archive I download daily:
```
40 10 * * * wget --content-disposition \
--header="Authorization: secretkeygoeshere" \
--directory-prefix /mnt/backup/remote/portalbak/ \
--no-verbose --append-output=/var/log/portalbackup.log \
https://api.my.protospace.ca/backup/
```
The website and [wiki](https://wiki.protospace.ca) that I sysadmin get
backed up weekly:
```
0 12 * * 1 mysqldump --all-databases > /var/www/dump.sql
15 12 * * 1 date -Iseconds > /var/www/backup_check.txt
20 12 * * 1 rdiff-backup /var/www pshostbak@remotebackup::/mnt/backup/remote/pshostbak/weekly/www/
```
The Protospace [Minecraft
server](http://games.protospace.ca:8123/?worldname=world&mapname=flat&zoom=3&x=74&y=64&z=354)
I run gets backed up daily:
```
00 15 * * * date -Iseconds > /home/tanner/minecraft/backup_check.txt
00 15 * * * rdiff-backup --exclude **CoreProtect --exclude **dynmap /home/tanner/minecraft psminebak@remotebackup::/mnt/backup/remote/psminebak/
30 15 * * * rdiff-backup --remove-older-than 12B --force psminebak@remotebackup::/mnt/backup/remote/psminebak/
```
I also back up our Google Drive with rclone:
```
45 12 * * 1 rclone copy -v protospace: /mnt/backup/files/protospace/google-drive/
```
## Backup Copies
My backup folder `/mnt/backup` now looks like this:
```
/mnt/backup/
├── files
│   ├── docs
│   ├── phone
│   ├── protospace
│   ├── telegram
│   ├── usbsticks
│   └── ... and so on
├── local
│   ├── email
│   ├── gitea
│   ├── nextcloud
│   └── notes
└── remote
├── portalbak
├── pshostbak
├── psminebak
├── tbotbak
└── telebak
```
This directory tree is the master backup and I make a copy of the entire tree
every Saturday to a hard drive.
The directory is copied over with the following script:
```text
#!/bin/bash
cryptsetup luksOpen /dev/sdf external
mount /dev/mapper/external /mnt/external
time rsync -av --delete /mnt/backup/local/ /mnt/external/backup/local/
time rsync -av --delete /mnt/backup/remote/ /mnt/external/backup/remote/
time rdiff-backup --force -v5 /mnt/backup/files/ /mnt/external/backup/files/
python3 /home/tanner/scripts/checkbackup.py
umount /mnt/external
cryptsetup luksClose external
```
I wrote a Python script `checkbackup.py` that goes through each backup and
compares the timestamp in `backup_check.txt` files to the current time. This
makes sure that the cron ran, backups were taken, and transferred over
correctly.
## Rotating Hard Drives
I rotate through 2.5" 1 TB hard drives each Saturday when I do a backup. They
are quite cheap at [$65 CAD](https://www.memoryexpress.com/Products/MX65194)
each so I can have a bunch floating around.
I keep one connected to the server, one in my bag, one offsite, one at my
mother's house, and one at my dad's house. Every Saturday I run the script above
to take a copy and then swap the drive with the one in my bag. It then gets
<span class="aside">I go back home about twice per year</span>
swapped when I visit my offsite location. Same for when I visit my parents. This
means that all hard drives eventually get rotated through with new data and
don't sit too long unpowered.
The drives are all encrypted with full-disk LUKS encryption using a password I'm
unlikely to forget.
I run the check-summing `btrfs` file system on them in RAID-1 to protect against
bitrot. This means I can only use 0.5 TB of storage for my backups, but the data
is stored redundantly.
Here's how I set up new hard drives to do this:
```
$ sudo cryptsetup luksOpen /dev/sdf external
$ sudo mkfs.btrfs -f -m dup -d dup /dev/mapper/external
$ sudo mount /dev/mapper/external /mnt/external/
$ sudo mkdir /mnt/external/backup
$ sudo chown -R tanner:tanner /mnt/external/backup
$ sudo umount /mnt/external
$ sudo cryptsetup luksClose external
```
## Future Improvements
I'm working on a system to automatically back up all my home directories to my
media server. I need this to grab Bash histories and code that's
work-in-progress. I've been burned by not having this once when a server died.
I'd like to automate backing up my phone by connecting it to a Raspberry Pi when
I go to sleep.
I need to get better at fully testing my backups by restoring them on a blank
machine.

392
content/bypassing-ports.md Normal file
View File

@@ -0,0 +1,392 @@
Title: Bypassing ISP Blocked Ports
Date: 2021-04-10
Category: Writing
Summary: Bypass ISP blocked ports using VPN port forwarding for public access.
Wide: true
[TOC]
My residential ISP blocks inbound traffic to common ports like 22, 80, and 443.
I use an OpenVPN tunnel to forward these ports so that I can self-host a
public media server. It does __not__ require users to be on the VPN.
This article explains how I set it up and is targeted towards Linux sysadmins.
## Overview
I have a cheap $5 per month virtual server with [Digital
Ocean](https://digitalocean.com) that runs Debian GNU/Linux 10. An OpenVPN
server is running on this virtual server.
My media server at home has an OpenVPN client connected to the server and is
assigned a static IP on the VPN network.
The virtual server has routing enabled and forwards inbound traffic __from the
internet__ to my media server at home. This allows me to have external HTTP and SSH
access.
## Server Setup
Spin up a Debian 10 virtual server on your favourite hosting provider and set
your user up as you would normally. You should probably harden this server.
Assign a subdomain to it like `vpn.example.com`.
Install the following requirements:
```
$ sudo apt update
$ sudo apt install openvpn ufw
```
### OpenVPN Server
These steps roughly follow [this
guide](https://wiki.debian.org/OpenVPN#TLS-enabled_VPN).
Generate TLS certificates and keys:
```
$ cd /etc/openvpn
$ sudo openvpn --genkey --secret static.key
$ sudo make-cadir easy-rsa/
$ sudo chown -R tanner:tanner easy-rsa/
```
Replace `tanner` with your own username, this is temporary.
<span class="aside">The `.rnd` file prevents a warning</span>
```
$ cd easy-rsa/
$ ./easyrsa init-pki
$ head /dev/urandom > pki/.rnd
$ ./easyrsa build-ca
```
Enter a password you won't forget in case you want to add another client later.
The Common Name you choose is not important.
Generate DiffieHellman params:
```
$ ./easyrsa gen-dh
```
Generate a server cert:
```
$ ./easyrsa build-server-full server nopass
```
Generate a client cert:
```
$ ./easyrsa build-client-full mediaserver nopass
```
We make a `mediaserver` client because we want to assign a static IP to it. You
need to make a different one for each client you want with a static IP.
Also, if you want generic clients that all get dynamic IPs for use on your
laptop, phone, etc. to protect you from public WiFi, create only a single extra one:
```
$ ./easyrsa build-client-full client nopass # optional
```
Leave off `nopass` if you want to password protect the config file keys when you
set up a new client.
Create the server config file `/etc/openvpn/server.conf`:
<span class="aside">Can't use port 443 here since it'll be forwarded</span>
```
port 1194
proto udp
dev tun
topology subnet
ca /etc/openvpn/easy-rsa/pki/ca.crt
cert /etc/openvpn/easy-rsa/pki/issued/server.crt
key /etc/openvpn/easy-rsa/pki/private/server.key
dh /etc/openvpn/easy-rsa/pki/dh.pem
tls-auth /etc/openvpn/static.key 0
client-config-dir /etc/openvpn/ccd
server 10.8.0.0 255.255.255.0
client-to-client
duplicate-cn
keepalive 10 120
cipher AES-256-GCM
auth SHA256
comp-lzo
max-clients 10
user nobody
group nogroup
persist-key
persist-tun
```
Assign a static IP + chmod:
```
$ cd /etc/openvpn
$ sudo chown -R root:root easy-rsa/
$ sudo mkdir ccd
$ sudo touch ccd/mediaserver
```
Replace `mediaserver` with whatever client name you used above. Edit it like so:
<span class="aside">Your home server will be `10.8.0.100`</span>
```
ifconfig-push 10.8.0.100 255.255.255.0
```
Test your config by running:
```
$ sudo openvpn --config /etc/openvpn/server.conf
```
If you run `ip addr` in another terminal, you should see an entry like this:
```
5: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> stuff
link/none
inet 10.8.0.1/24 brd 10.8.0.255 scope global tun0
valid_lft forever preferred_lft forever
inet6 fe80::d9fc:b2f9:34e6:5ed2/64 scope link stable-privacy
valid_lft forever preferred_lft forever
```
### systemd
If it works fine, persist OpenVPN with systemd:
```
$ sudo systemctl enable openvpn@server
$ sudo systemctl start openvpn@server
$ sudo systemctl daemon-reload
$ sudo service openvpn restart
```
Test it works by rebooting:
```
$ sudo reboot
$ ssh vpn.example.com
$ ip addr
```
### Port Forwarding
I use `ufw` to handle the iptables rules because I use it anyway as a firewall
when I harden my servers.
Enable routing:
```
$ sudo sysctl net.ipv4.ip_forward=1
```
Edit `/etc/sysctl.conf` to set:
```
net.ipv4.ip_forward=1
```
Edit `/etc/default/ufw` to set:
```
DEFAULT_FORWARD_POLICY="ACCEPT"
```
Add this to the top of `/etc/ufw/before.rules`:
```
*nat
:POSTROUTING ACCEPT [0:0]
# ssh port forwarding
-A PREROUTING -d 123.123.123.123 -p tcp --dport 2222 -j DNAT --to-dest 10.8.0.100:2222
-A POSTROUTING -d 10.8.0.100 -p tcp --dport 2222 -j SNAT --to-source 10.8.0.1
# Allow traffic from OpenVPN client to eth0
-A POSTROUTING -s 10.8.0.0/8 -o eth0 -j MASQUERADE
COMMIT
```
Replace `123.123.123.123` with your VPN server's external IP address and `eth0`
with the external interface.
This will forward TCP traffic on port 2222 to your home server. If you want to use
port 22, then you need to set the VPN SSH server to something else.
A full example of `/etc/ufw/before.rules` with other ports included can be found
here:
[https://txt.t0.vc/URUG](https://txt.t0.vc/URUG)
Apply the changes to `ufw`:
```
$ sudo ufw disable && sudo ufw enable
```
## Client Setup
Switch to your home server or client machine.
Install OpenVPN:
```
$ sudo apt update
$ sudo apt install openvpn
```
### Client Configs
For static IP clients (like your home server), create the config file `/etc/openvpn/client.conf`:
```
client
dev tun
proto udp
remote vpn.example.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
cipher AES-256-GCM
auth SHA256
comp-lzo
key-direction 1
<ca>
[server /etc/openvpn/easy-rsa/pki/ca.crt]
</ca>
<cert>
[server /etc/openvpn/easy-rsa/pki/issued/mediaserver.crt]
</cert>
<key>
[server /etc/openvpn/easy-rsa/pki/private/mediaserver.key]
</key>
<tls-auth>
[server /etc/openvpn/static.key]
</tls-auth>
```
Replace the `[server ...]` lines with the contents of that file on the __VPN
server__, for example:
```
$ sudo cat /etc/openvpn/easy-rsa/pki/ca.crt
---> copy & paste result
```
Also replace `vpn.example.com` with the subdomain you assigned earlier.
For device clients (like your laptop and phone), create the config file `client.ovpn`:
<span class="aside">`redirect-gateway def1` forces traffic over the VPN</span>
```
client
dev tun
proto udp
remote vpn.example.com 1194
redirect-gateway def1
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
cipher AES-256-GCM
auth SHA256
comp-lzo
key-direction 1
<ca>
[server /etc/openvpn/easy-rsa/pki/ca.crt]
</ca>
<cert>
[server /etc/openvpn/easy-rsa/pki/issued/client.crt]
</cert>
<key>
[server /etc/openvpn/easy-rsa/pki/private/client.key]
</key>
<tls-auth>
[server /etc/openvpn/static.key]
</tls-auth>
```
The `client.ovpn` file is ready to be imported into your VPN clients.
Test your config by running:
```
$ sudo openvpn --config /etc/openvpn/client.conf
```
If you run `ip addr` in another terminal, you should see an entry like this:
```
7: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> stuff
link/none
inet 10.8.0.100/24 brd 10.8.0.255 scope global tun0
valid_lft forever preferred_lft forever
inet6 fe80::b2:ed71:6c98:4bc9/64 scope link stable-privacy
valid_lft forever preferred_lft forever
```
Try pinging the server:
```
$ ping 10.8.0.1
PING 10.8.0.1 (10.8.0.1) 56(84) bytes of data.
64 bytes from 10.8.0.1: icmp_seq=1 ttl=64 time=71.5 ms
64 bytes from 10.8.0.1: icmp_seq=2 ttl=64 time=73.0 ms
... etc
```
### systemd
If it works fine, persist OpenVPN with systemd:
```
$ sudo chown root:root /etc/openvpn/client.conf
$ sudo chmod 600 /etc/openvpn/client.conf
$ sudo systemctl enable openvpn@client
$ sudo systemctl start openvpn@client
$ sudo systemctl daemon-reload
$ sudo service openvpn restart
```
### Client Apps
On Android I use "OpenVPN for Android" and on Linux I use the
`network-manager-openvpn-gnome` Debian package.
To add your VPN on Gnome, open VPN settings, import file, and select
`client.ovpn`. If the private key is missing, select it from
`~/.cert/nm-openvpn/`.
## Closing Thoughts
You should now be fine to access your home server from over the internet.
To forward additional ports, just edit the `/etc/ufw/before.rules` file like
above.
You can now point a domain to your virtual server's IP and use that to connect
to your home server. Use a CNAME to make it easy to change later:
```
NAME TYPE VALUE
--------------------------------------------------
vpn.example.com. A 123.123.123.123
myserver.example.com. CNAME vpn.example.com.
```
Finally, make sure any server programs are listening / bound to `10.8.0.100` or
`0.0.0.0` so that they can get traffic from that interface.

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🏴‍☠️</text></svg>

After

Width:  |  Height:  |  Size: 119 B

View File

@@ -0,0 +1,28 @@
Title: Hand of Ozymandias
Date: 2012-03-23
Category: Projects
Summary: A withered hand I welded out of scrap metal.
I was visiting my cousins in Radium, BC and decided to learn stick welding at
their shop. I wanted to create a sculpture, so with pieces of scrap metal I
welded together this hand. The beads are far from perfect. Working with small
pieces of rusted metal made it difficult.
![a rusted hand welded together out of scrap square stock metal tubing]({static}/images/hand-of-ozymandias/hand1.jpg)
## The Name
One of my favourite poems is [Ozymandias](https://en.wikipedia.org/wiki/Ozymandias)
by Percy Bysshe Shelley. It's about the inevitable complete decline of all
rulers and the empires they build, however mighty in their time. This is the
hand of Ozymandias sticking out from the sand, grasping for life after he has
been reduced to dust.
## Construction
I eyeballed the joint angles and my cousin cut them to spec with an angle
grinder. It was made in a machine shop with no real planning done ahead of time.
In between welds, I used my own hand as a reference. Below is a picture of me
adding a bead to it.
![me welding the hand causing a very bright white light that washes out the photo]({static}/images/hand-of-ozymandias/hand2.jpg)

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

28
content/led-dress.md Normal file
View File

@@ -0,0 +1,28 @@
Title: LED Dress
Date: 2016-03-18
Category: Projects
Summary: A dress made out of LEDs that twinkle like stars.
A friend of mine was attending a stars and constellations themed ball. She
wanted to wear a dress that was lit up with LEDs acting as twinkling stars.
Seven of the 28 stars are aligned to resemble the Big Dipper constellation and
twinkle differently than the rest, which twinkle in a random pattern.
![a girl wearing a blue dress with a number of LEDs shining through the fabric]({static}/images/dress/dress1.jpg)
## Construction
The LEDs came from that strip that was cut up and soldered together with very
small wires. Each of the LEDs can be controlled individually.
![the controller circuit board, and the string of soldered together LEDs]({static}/images/dress/dress2.jpg)
21 of the stars are light magenta in color and twinkle by fading randomly. The
seven LEDs that form the Big Dipper continually scroll through a gradient of
three colors. Instead of calculating the values of each color in the gradient as
the program runs, a lookup table is used.
<video autoplay muted loop style="display:block; margin: 0 auto;">
<source src="{static}/videos/dress/dress3.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

33
content/light-switch.md Normal file
View File

@@ -0,0 +1,33 @@
Title: Remote Control Light Switch
Date: 2014-10-09
Category: Projects
Summary: A device to toggle my lights remotely.
I wanted the ability to toggle my bedroom light remotely for convenience. I
designed a circuit that allows me to control my light with any
device that can load a webpage.
I still wanted to be able to control the light manually, so I bought a metallic
face-plate and turned it into a capacitive touch sensor. The slightest touch
anywhere on the plate is enough to toggle the light. I had to electrically
isolate the metal screws from it because they screw into a grounded switch box.
![my custom light switch, a Raspberry Pi computer, and an old iPhone]({static}/images/light-switch/light1.jpg)
## Function
I have a Raspberry Pi ($35 computer) on my home network that runs a web server.
When you connect to it in your web browser, a page loads with buttons to turn
the light on or off. When you press a button, the server executes a command that
sends a message over Bluetooth to the light switch. The Bluetooth module in the
wall receives this message and forwards it to the microcontroller, which
processes it and toggles the relay. The whole circuit is also powered from mains
by an AC-DC converter.
This entire process happens quicker than half a second, so it feels instant.
![the front side which has several electrical components]({static}/images/light-switch/light2.jpg)
<span class="aside">Black stuff's liquid electrical tape</span>
![the back side which has wires soldered to connect all the components]({static}/images/light-switch/light3.jpg)

106
content/linux-flavour.md Normal file
View File

@@ -0,0 +1,106 @@
Title: Choosing a Linux Flavour
Date: 2020-10-31
Category: Writing
Summary: A recommendation on which flavour of Linux to run.
Wide: true
[TOC]
People often ask me which flavour of Linux they should install. In summary,
choose Ubuntu if it's your first time. Once you are comfortable, install Debian
the next time you need to install Linux.
I run Debian on my computers and servers.
## Linux Distributions
When people refer to the "flavour of Linux" they are talking about a Linux
<span class="aside">Interjection: it's technically called GNU/Linux</span>
distribution (distro). It mostly describes what software is distributed in its
software repository.
"A typical Linux distribution comprises a Linux kernel, GNU tools and
libraries, additional software, documentation, a window system, a window
manager, and a desktop environment." [Wikipedia]
The major Linux distros are practically all the same. If you master one it's
easy to pick up the others. The main differences you'll run into are which
tools you use to install new software, and the desktop environment, which is
what all the windows and buttons look like.
I recommend two Linux distros, Debian and Ubuntu. Ubuntu is based off of
Debian, so they are very similar.
## Pros of Debian
Debian is one of the oldest distros and many other distros are based off it.
You can see a timeline visualization of all its derivatives here:
<https://upload.wikimedia.org/wikipedia/commons/1/1b/Linux_Distribution_Timeline.svg>
This image is what originally convinced me to use Debian. Scroll down until you
see it and zoom out so you grasp how many derivatives it has.
Debian is also non-commercial and requires that all software in its main
repository is free and open source. This is important because that grants you
the right to study, change, and distribute the software and source code to
anyone and for any purpose. They also follow a strong social contract you can
see here:
<https://www.debian.org/social_contract>
It's also a very stable Linux distro since they freeze all software features on
each release. This makes it great for servers because nothing will break when
it updates.
The main Raspberry Pi distro is nearly identical to Debian, so you'll also gain
familiarity with it.
## Cons of Debian
Since Debian requires all its software to be free and open source, proprietary
hardware drivers aren't included in its main repo. This can make installing
Debian difficult if your hardware requires non-free drivers. You'll need to use
a non-free installation image found here:
<https://cdimage.debian.org/cdimage/unofficial/non-free/cd-including-firmware/>
The fact that Debian freezes software features can also mean that your software
gets old until the next Debian release. If you want versions that are bleeding
edge, you'll need to use Debian Unstable as described here:
<https://wiki.debian.org/DebianUnstable#Installation>
Don't be fooled by the name "unstable". I use it for my personal computers and
it runs fine.
## Pros of Ubuntu
Ubuntu is incredibly easy to install. You can also try it out before deciding
to install it. The distro pretty much just works on what ever hardware you
have.
It's very beginner friendly because it's so popular. Any problem you search for
will reveal dozens of threads with people solving the same problem.
## Cons of Ubuntu
Unfortunately Ubuntu is developed by a commercial company, Canonical. The
company's interests come first, before the users' and they have a track record
of betraying their users' trust and privacy.
Years ago Ubuntu had a feature enabled by default that would send your desktop
searches to Amazon so they could suggest products for you to buy:
<https://www.pcworld.com/article/2840401/ubuntus-unity-8-desktop-removes-the-amazon-search-spyware.html>
Currently whenever you remote login to your Ubuntu machine, it phones home to
Canonical and they collect info about your system:
<https://ubuntu.com/legal/motd>
While these reasons are fairly minor, they are quite frowned upon in the Linux
community and are reason enough to switch to Debian once you are comfortable
with using Linux.
[Wikipedia]: https://en.wikipedia.org/wiki/Linux_distribution

40
content/painting.md Normal file
View File

@@ -0,0 +1,40 @@
Title: Mans Reach Exceeds His Grasp
Date: 2012-04-11
Category: Projects
Summary: My first attempt at painting with acrylic.
The painting is called “Mans Reach Exceeds His Grasp”. I've always wanted to
try painting and thought I had a good idea, so after a couple of drawings I
attempted to paint it. I eventually got it framed at Michaels. Many thanks to my
friend Laura for the opportunity to do this, I couldn't have done it without her
help.
![a painting of water pouring out of a vase and into a hand, then turning to sand]({static}/images/painting/painting1.jpg)
## The Meaning
Its hard to see in the photo, but the moment the water touches his hand it
turns into sand and is taken by the slight breeze. The title is a quote from
Andrea del Sarto, a poem by Robert Browning. It is also said by Nikola Teslas
character in my favourite movie, [The Prestige](https://www.imdb.com/title/tt0482571/).
“I, painting from myself and to myself,
Know what I do, am unmoved by mens blame
Or their praise either. Somebody remarks
Morello's outline there is wrongly traced,
His hue mistaken; what of that? or else,
Rightly traced and well ordered; what of that?
Speak as they please, what does the mountain care?
Ah, but a mans reach should exceed his grasp,
Or whats a heaven for?”
Robert Browning from *Andrea del Sarto*
## Creation
I started with the background, trying to make it blurry and out of focus, then
slowly progressed to the foreground. The hands were drawn in pencil and painted
in. It was quite difficult to get the blending and shadows perfect, but I had
Laura to tell me when things didn't look right. Below I am trying to figure out
what a hand looks like in a mirror.
![me looking at my own hand in the mirror as a guide]({static}/images/painting/painting2.jpg)

41
content/plant-waterer.md Normal file
View File

@@ -0,0 +1,41 @@
Title: Automatic Plant Waterer
Date: 2014-06-05
Category: Projects
Summary: A device that automatically waters plants.
One day I decided watering my one plant was too much work, so I automated it.
It's also great for when I'm on vacation. The plant is a year old now and
doesn't look as good as it used to (kinda like you). So this machine is like its
life support.
<span class="aside">Update: this plant died long ago</span>
![the device and pump on a 2L pop bottle with a tube running to a flowerpot]({static}/images/plant-waterer/waterer1.jpg)
## First Attempt
The design was very simple and soldered together on perf board. A
microcontroller turns the pump on for 20 seconds, then waits 24 hours and
restarts. The pump ran way too fast so it was slowed down to 10% power.
This design suffered from a fatal problem. After running, there was a chance
that the tube would stay full of fluid. If the water level in the pop bottle was
too high, it could siphon out. I woke up with a flower pot overflowing with
water a couple of times.
![a new version feeding into a different plant]({static}/images/plant-waterer/waterer2.jpg)
## Second Attempt
I liked the idea so much that I made a second iteration. This one used a custom
printed circuit board with a lot more features. The pumping duration could be
adjusted with a screwdriver. This was useful as the plant (now a
[Ming aralia](https://en.wikipedia.org/wiki/Polyscias_fruticosa)) grew.
Another feature was the ability to run the pump backwards. This completely
eliminated the siphoning problem from before. After pumping for a set duration,
it would run backwards until the tube was cleared of water.
<span class="aside">Also dead :(</span>
![the new version beside a big Ming aralia plant with bushy drooping leaves and skinny stems]({static}/images/plant-waterer/waterer3.jpg)

23
content/solar-car.md Normal file
View File

@@ -0,0 +1,23 @@
Title: Solar Car
Date: 2013-04-27
Category: Projects
Summary: About my time volunteering with the University of Calgary Solar Car Team, where I designed a maximum power point tracker.
I joined the University of Calgary Solar Car Team in my first semester for a
chance to learn things, gain practical experience, and meet people that share my
interests. The car was the top Canadian team in a 3000 km race from Darwin to
Adelaide, Australia in 2011. We met up at a shop on campus every Saturday
morning to work on the new Generation IV of the solar car.
![the MPPT device, a printed circuit board with bulky round electrical components held in my hand]({static}/images/solar-car/solar1.jpg)
## The Helianthus MPPT
I was in charge of designing and assembling the MPPTs (maximum power point
trackers) for the new generation solar car. An MPPT extracts as much power out
of the solar cells as possible. The solar array operates less efficiently
without them. The Generation IV car, Schulich Delta (pictured below) uses seven
of them: one per section of solar cells with similar lighting conditions. Andrei
and I designed the MPPT above.
![the solar car from an angle with a driver inside]({static}/images/solar-car/solar2.jpg)

Binary file not shown.

View File

@@ -1,18 +0,0 @@
{
"name": "my-razzle-app",
"version": "2.0.0-alpha.8",
"license": "MIT",
"scripts": {
"start": "razzle start",
"build": "razzle build",
"test": "razzle test --env=jsdom",
"start:prod": "NODE_ENV=production node build/server.js"
},
"dependencies": {
"express": "^4.16.3",
"razzle": "^2.2.0",
"react": "^16.4.1",
"react-dom": "^16.4.1",
"react-router-dom": "^4.3.1"
}
}

57
pelicanconf.py Normal file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- #
from __future__ import unicode_literals
AUTHOR = 'Tanner Collin'
SITENAME = 'Tanner Collin'
SITEURL = ''
PATH = 'content'
TIMEZONE = 'Canada/Mountain'
DEFAULT_LANG = 'en'
# Feed generation is usually not desired when developing
FEED_ALL_ATOM = None
CATEGORY_FEED_ATOM = None
TRANSLATION_FEED_ATOM = None
AUTHOR_FEED_ATOM = None
AUTHOR_FEED_RSS = None
DEFAULT_PAGINATION = False
MARKDOWN = {
'extension_configs': {
'markdown.extensions.codehilite': {'css_class': 'highlight'},
'markdown.extensions.extra': {},
'markdown.extensions.meta': {},
'markdown.extensions.toc': {
'toc_depth': '2-3',
'anchorlink': True,
},
},
'output_format': 'html5',
}
STATIC_PATHS = ['images', 'extra']
EXTRA_PATH_METADATA = {
'extra/favicon.svg': {'path': 'favicon.svg'},
}
# Uncomment following line if you want document-relative URLs when developing
#RELATIVE_URLS = True
THEME = 'themes/theme'
# turn off useless outputs
TAG_SAVE_AS = ''
CATEGORY_SAVE_AS = ''
AUTHOR_SAVE_AS = ''
ARCHIVES_SAVE_AS = ''
AUTHORS_SAVE_AS = ''
CATEGORIES_SAVE_AS = ''
TAGS_SAVE_AS = ''
PROD = False

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -1,48 +0,0 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

View File

@@ -1,2 +0,0 @@
User-agent: *

63
publishconf.py Normal file
View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- #
from __future__ import unicode_literals
AUTHOR = 'Tanner Collin'
SITENAME = 'Tanner Collin'
SITEURL = ''
PATH = 'content'
TIMEZONE = 'Canada/Mountain'
DEFAULT_LANG = 'en'
# Feed generation is usually not desired when developing
FEED_ALL_ATOM = None
CATEGORY_FEED_ATOM = None
TRANSLATION_FEED_ATOM = None
AUTHOR_FEED_ATOM = None
AUTHOR_FEED_RSS = None
DEFAULT_PAGINATION = False
MARKDOWN = {
'extension_configs': {
'markdown.extensions.codehilite': {'css_class': 'highlight'},
'markdown.extensions.extra': {},
'markdown.extensions.meta': {},
'markdown.extensions.toc': {
'toc_depth': '2-3',
'anchorlink': True,
},
},
'output_format': 'html5',
}
STATIC_PATHS = ['images', 'extra']
EXTRA_PATH_METADATA = {
'extra/favicon.svg': {'path': 'favicon.svg'},
}
# Uncomment following line if you want document-relative URLs when developing
#RELATIVE_URLS = True
THEME = 'themes/theme'
# turn off useless outputs
TAG_SAVE_AS = ''
CATEGORY_SAVE_AS = ''
AUTHOR_SAVE_AS = ''
ARCHIVES_SAVE_AS = ''
AUTHORS_SAVE_AS = ''
CATEGORIES_SAVE_AS = ''
TAGS_SAVE_AS = ''
INDEX_SAVE_AS = 'index.html'
ARTICLE_URL = '{slug}/'
ARTICLE_SAVE_AS = '{slug}/index.html'
PAGE_URL = '{slug}/'
PAGE_SAVE_AS = '{slug}/index.html'
PROD = True

View File

@@ -1,10 +0,0 @@
* {
box-sizing: border-box;
}
body {
line-height: 1.6;
font-size: 1.125rem;
color: #333;
background-color: #F9F9F9;
}

View File

@@ -1,13 +0,0 @@
import React from 'react';
import Route from 'react-router-dom/Route';
import Switch from 'react-router-dom/Switch';
import Home from './Home';
import './App.css';
const App = () => (
<Switch>
<Route exact path='/' component={Home} />
</Switch>
);
export default App;

View File

@@ -1,191 +0,0 @@
.grid-container {
background: #bad4ff;
max-width: 120rem;
margin: 0 auto;
}
.grid-item {
//background: linear-gradient(135deg, #87e0fd 0%,#53cbf1 40%,#05abe0 100%);
position: relative;
float: left;
width: 30rem;
min-width: 25rem;
min-height: 25rem;
padding-bottom: 30rem;
}
.grid-item-content {
position: absolute;
width: 100%;
height: 100%;
padding: 1rem;
}
.menu {
display: flex;
flex-flow: column;
}
.menu .logo {
object-fit: contain;
padding-top: 1em;
min-height: 5rem;
}
.menu section {
min-height: 0;
flex: 1;
}
.menu section aside {
position: relative;
top: 50%;
transform: translateY(-50%);
text-align: right;
}
.menu section img {
height: 100%;
float: right;
padding: 2rem;
}
.project {
display: flex;
flex-flow: column;
}
.project header {
font-size: 2.25rem;
line-height: 1.2;
}
.project section {
min-height: 0;
flex: 1;
}
.project.bottom section {
flex-flow: column;
display: flex;
}
.project.left section p {
position: relative;
top: 50%;
transform: translateY(-50%);
}
.project.left section img {
height: 100%;
float: right;
padding: 1rem;
}
.project.bottom section p {
margin-bottom: 1rem;
}
.project.bottom section img {
object-fit: contain;
min-height: 0;
padding: 1rem;
}
.project footer {
display: flex;
justify-content: space-between;
margin: 0 1em;
font-size: 1rem;
font-family: monospace;
}
.photo a {
display: block;
position: relative;
height: 100%;
}
.photo header, .photo footer {
display: none;
position: absolute;
width: 100%;
z-index: 2;
background: rgba(0, 0, 0, 0.4);
color: white;
}
.photo header {
top: 0;
font-size: 1.5rem;
padding: 0.5rem;
}
.photo footer {
bottom: 0;
justify-content: space-between;
padding: 0 1em;
font-size: 1rem;
font-family: monospace;
}
.photo a:hover header {
display: block;
}
.photo a:hover footer {
display: flex;
}
.photo img {
position: absolute;
height: 100%;
left: 0;
}
@media all and (max-width: 120rem) {
.grid-item {
width: 25%;
padding-bottom: 25%
}
}
@media all and (max-width: 102rem) {
.grid-item {
width: 33.33%;
padding-bottom: 33.33%;
}
}
@media all and (max-width: 76.5rem) {
.grid-container {
max-width: 68rem;
}
.grid-item {
width: 50%;
padding-bottom: 50%;
}
}
@media all and (max-width: 51rem) {
.grid-container {
max-width: 34rem;
}
.grid-item {
width: 100%;
padding-bottom: 100%;
margin-bottom: 1rem;
}
.grid-item-content.photo {
padding: 0;
}
.photo a header {
display: block;
}
.photo a footer {
display: flex;
}
}

View File

@@ -1,125 +0,0 @@
import React from 'react';
import './Grid.css';
import logo from './logo.png';
import GridItem from './GridItem';
import tanner from './tanner.jpg';
import dress1 from './dress1.jpg';
import switch1 from './switch1.jpg';
import pump1 from './pump1.jpg';
import hand1 from './hand1.jpg';
import banff1 from './banff1.jpg';
import bee1 from './bee1.jpg';
import canmore1 from './canmore1.jpg';
import carshow1 from './carshow1.jpg';
//function importAll(r) {
// return r.keys().map(x => r(x));
//}
//const images = importAll(require.context('./images'));
class Grid extends React.Component {
render() {
return (
<div className='grid-container'>
<GridItem type='menu'>
<img src={logo} className='logo' alt='' />
<section>
<img src={tanner} alt='' />
<aside>
<p>Home</p>
<p>Projects</p>
<p>Photograhy</p>
<p>Art</p>
<p>About</p>
<p>Contact</p>
</aside>
</section>
<footer>More info or a .onion link.</footer>
</GridItem>
<GridItem type='project left'>
<header>LED Dress</header>
<section>
<img src={dress1} alt='' />
<p>A dress lit up with LEDs that act as twinkling stars. Seven act as The Big Dipper, and the rest twinkle randomly.</p>
</section>
<footer>
<p>Project (Hardware)</p><p>2018-10-11</p>
</footer>
</GridItem>
<GridItem type='photo'>
<a>
<header>Thorncliffe Car Show</header>
<img src={carshow1} alt='' />
<footer>
<p>Photography</p><p>2018-10-11</p>
</footer>
</a>
</GridItem>
<GridItem type='project bottom'>
<header>Remote Control Lightswitch</header>
<section>
<img src={switch1} alt='' />
<p>Custom circuit that lets me control a ceiling light over Wifi.</p>
</section>
<footer>
<p>Project (Hardware)</p><p>2018-10-11</p>
</footer>
</GridItem>
<GridItem type='project bottom'>
<header>Plant Waterer</header>
<section>
<img src={pump1} alt='' />
<p>Controller that waters my plant automatically every day.</p>
</section>
<footer>
<p>Project (Hardware)</p><p>2018-10-11</p>
</footer>
</GridItem>
<GridItem type='photo'>
<a>
<header>Prince's Island Park</header>
<img src={bee1} alt='' />
<footer>
<p>Photography</p><p>2018-10-11</p>
</footer>
</a>
</GridItem>
<GridItem type='photo'>
<a>
<header>Johnston Creek, Banff</header>
<img src={banff1} alt='' />
<footer>
<p>Photography</p><p>2018-10-11</p>
</footer>
</a>
</GridItem>
<GridItem type='project left'>
<header>Hand of Ozymandias</header>
<section>
<img src={hand1} alt='' />
<p>Sculpture welded together out of scrap metal for practice.</p>
</section>
<footer>
<p>Project (Hardware)</p><p>2018-10-11</p>
</footer>
</GridItem>
<GridItem type='photo'>
<a>
<header>VW Van in Canmore</header>
<img src={canmore1} alt='' />
<footer>
<p>Photography</p><p>2018-10-11</p>
</footer>
</a>
</GridItem>
</div>
);
}
}
export default Grid;

View File

@@ -1,15 +0,0 @@
import React from 'react';
class GridItem extends React.Component {
render() {
return (
<div className='grid-item'>
<div className={'grid-item-content ' + this.props.type}>
{this.props.children}
</div>
</div>
);
}
}
export default GridItem;

View File

@@ -1,3 +0,0 @@
.Home {
text-align: center;
}

View File

@@ -1,16 +0,0 @@
import React from 'react';
import './Home.css';
import Grid from './Grid';
class Home extends React.Component {
render() {
return (
<div className='Home'>
<Grid />
</div>
);
}
}
export default Home;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 KiB

View File

@@ -1,15 +0,0 @@
import App from './App';
import BrowserRouter from 'react-router-dom/BrowserRouter';
import React from 'react';
import { hydrate } from 'react-dom';
hydrate(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
if (module.hot) {
module.hot.accept();
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -1,26 +0,0 @@
import app from './server';
import http from 'http';
const server = http.createServer(app);
let currentApp = app;
server.listen(process.env.PORT || 3000, error => {
if (error) {
console.log(error);
}
console.log('🚀 started');
});
if (module.hot) {
console.log('✅ Server-side HMR Enabled!');
module.hot.accept('./server', () => {
console.log('🔁 HMR Reloading `./server`...');
server.removeListener('request', currentApp);
const newApp = require('./server').default;
server.on('request', newApp);
currentApp = newApp;
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -1,52 +0,0 @@
import App from './App';
import React from 'react';
import { StaticRouter } from 'react-router-dom';
import express from 'express';
import { renderToString } from 'react-dom/server';
const assets = require(process.env.RAZZLE_ASSETS_MANIFEST);
const server = express();
server
.disable('x-powered-by')
.use(express.static(process.env.RAZZLE_PUBLIC_DIR))
.get('/*', (req, res) => {
const context = {};
const markup = renderToString(
<StaticRouter context={context} location={req.url}>
<App />
</StaticRouter>
);
if (context.url) {
res.redirect(context.url);
} else {
res.status(200).send(
`<!doctype html>
<html lang="">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta charset="utf-8" />
<title>Tanner Collin</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="reset.css">
${
assets.client.css
? `<link rel="stylesheet" href="${assets.client.css}">`
: ''
}
${
process.env.NODE_ENV === 'production'
? `<script src="${assets.client.js}" defer></script>`
: `<script src="${assets.client.js}" defer crossorigin></script>`
}
</head>
<body>
<div id="root">${markup}</div>
</body>
</html>`
);
}
});
export default server;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub icon</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>

After

Width:  |  Height:  |  Size: 827 B

View File

@@ -0,0 +1,2 @@
/*! instant.page v5.1.0 - (C) 2019-2020 Alexandre Dieulot - https://instant.page/license */
let t,e;const n=new Set,o=document.createElement("link"),i=o.relList&&o.relList.supports&&o.relList.supports("prefetch")&&window.IntersectionObserver&&"isIntersecting"in IntersectionObserverEntry.prototype,s="instantAllowQueryString"in document.body.dataset,a="instantAllowExternalLinks"in document.body.dataset,r="instantWhitelist"in document.body.dataset,c="instantMousedownShortcut"in document.body.dataset,d=1111;let l=65,u=!1,f=!1,m=!1;if("instantIntensity"in document.body.dataset){const t=document.body.dataset.instantIntensity;if("mousedown"==t.substr(0,"mousedown".length))u=!0,"mousedown-only"==t&&(f=!0);else if("viewport"==t.substr(0,"viewport".length))navigator.connection&&(navigator.connection.saveData||navigator.connection.effectiveType&&navigator.connection.effectiveType.includes("2g"))||("viewport"==t?document.documentElement.clientWidth*document.documentElement.clientHeight<45e4&&(m=!0):"viewport-all"==t&&(m=!0));else{const e=parseInt(t);isNaN(e)||(l=e)}}if(i){const n={capture:!0,passive:!0};if(f||document.addEventListener("touchstart",function(t){e=performance.now();const n=t.target.closest("a");if(!h(n))return;v(n.href)},n),u?c||document.addEventListener("mousedown",function(t){const e=t.target.closest("a");if(!h(e))return;v(e.href)},n):document.addEventListener("mouseover",function(n){if(performance.now()-e<d)return;const o=n.target.closest("a");if(!h(o))return;o.addEventListener("mouseout",p,{passive:!0}),t=setTimeout(()=>{v(o.href),t=void 0},l)},n),c&&document.addEventListener("mousedown",function(t){if(performance.now()-e<d)return;const n=t.target.closest("a");if(t.which>1||t.metaKey||t.ctrlKey)return;if(!n)return;n.addEventListener("click",function(t){1337!=t.detail&&t.preventDefault()},{capture:!0,passive:!1,once:!0});const o=new MouseEvent("click",{view:window,bubbles:!0,cancelable:!1,detail:1337});n.dispatchEvent(o)},n),m){let t;(t=window.requestIdleCallback?t=>{requestIdleCallback(t,{timeout:1500})}:t=>{t()})(()=>{const t=new IntersectionObserver(e=>{e.forEach(e=>{if(e.isIntersecting){const n=e.target;t.unobserve(n),v(n.href)}})});document.querySelectorAll("a").forEach(e=>{h(e)&&t.observe(e)})})}}function p(e){e.relatedTarget&&e.target.closest("a")==e.relatedTarget.closest("a")||t&&(clearTimeout(t),t=void 0)}function h(t){if(t&&t.href&&(!r||"instant"in t.dataset)&&(a||t.origin==location.origin||"instant"in t.dataset)&&["http:","https:"].includes(t.protocol)&&("http:"!=t.protocol||"https:"!=location.protocol)&&(s||!t.search||"instant"in t.dataset)&&!(t.hash&&t.pathname+t.search==location.pathname+location.search||"noInstant"in t.dataset))return!0}function v(t){if(n.has(t))return;const e=document.createElement("link");e.rel="prefetch",e.href=t,document.head.appendChild(e),n.add(t)}

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M467,61H45C20.218,61,0,81.196,0,106v300c0,24.72,20.128,45,45,45h422c24.72,0,45-20.128,45-45V106
C512,81.28,491.872,61,467,61z M460.786,91L256.954,294.833L51.359,91H460.786z M30,399.788V112.069l144.479,143.24L30,399.788z
M51.213,421l144.57-144.57l50.657,50.222c5.864,5.814,15.327,5.795,21.167-0.046L317,277.213L460.787,421H51.213z M482,399.787
L338.213,256L482,112.212V399.787z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 959 B

BIN
themes/theme/static/me.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -0,0 +1 @@
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Telegram icon</title><path d="M23.91 3.79L20.3 20.84c-.25 1.21-.98 1.5-2 .94l-5.5-4.07-2.66 2.57c-.3.3-.55.56-1.1.56-.72 0-.6-.27-.84-.95L6.3 13.7l-5.45-1.7c-1.18-.35-1.19-1.16.26-1.75l21.26-8.2c.97-.43 1.9.24 1.53 1.73z"/></svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@@ -0,0 +1,62 @@
{% extends "base.html" %}
{% block title %}{{ SITENAME }} - {{ article.title|striptags }}{% endblock %}
{% block head %}
{{ super() }}
{% if article.date %}
<meta name="date" content="{{article.date}}" />
{% endif %}
{% if article.summary %}
<meta name="description" content="{{article.summary|striptags}}" />
<meta name="summary" content="{{article.summary|striptags}}" />
{% endif %}
{% if article.category %}
<meta name="category" content="{{article.category}}" />
{% endif %}
{% for tag in article.tags %}
<meta name="tags" content="{{tag}}" />
{% endfor %}
{% endblock %}
{% block info %}
Tanner Collin
<p class="contact-icons">
<a href="mailto:site2@tannercollin.com" rel="noreferrer noopener"><img alt="email" src="/theme/mail.svg" width="20" height="20" /></a>
<a href="https://t.me/tannercollin" target="_blank" rel="noreferrer noopener"><img alt="telegram" src="/theme/telegram.svg" width="20" height="20" /></a>
<a href="https://github.com/tannercollin" target="_blank" rel="noreferrer noopener"><img alt="github" src="/theme/github.svg" width="20" height="20" /></a>
</p>
{% endblock %}
{% block content %}
{% if article.wide %}
<div class="content content-wide">
{% else %}
<div class="content">
{% endif %}
<p><a href="/">← Return to Home</a></p>
<header>
<h1>{{ article.title }}</h1>
<div class="summary">
{{ article.summary }}
</div>
<p class="metadata">
{{ article.locale_date }}
{% if article.modified %}
— updated {{ article.locale_modified }}
{% endif %}
</p>
</header>
<hr />
<article>
{{ article.content }}
</article>
</div>
{% endblock %}

View File

@@ -0,0 +1,57 @@
{% if PROD %}
<?php
$themeClass = '';
if (!empty($_COOKIE['theme'])) {
$themeClass = $_COOKIE['theme'];
}
?>
{% endif %}
<!DOCTYPE html>
<html lang="en">
<head>
{% block head %}
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ SITENAME }}{% endblock title %}</title>
<meta name="author" content="{{ AUTHOR }}" />
{% endblock head %}
<link rel="preload" href="/theme/fonts/AppSILB.ttf" as="font" type="font/ttf" crossorigin="anonymous">
<link rel="preload" href="/theme/fonts/AppSILBI.ttf" as="font" type="font/ttf" crossorigin="anonymous">
<link rel="preload" href="/theme/fonts/AppSILI.ttf" as="font" type="font/ttf" crossorigin="anonymous">
<link rel="preload" href="/theme/fonts/AppSILR.ttf" as="font" type="font/ttf" crossorigin="anonymous">
<link rel="preload" href="/theme/fonts/Lato-Bold.ttf" as="font" type="font/ttf" crossorigin="anonymous">
<link rel="preload" href="/theme/fonts/Lato-BoldItalic.ttf" as="font" type="font/ttf" crossorigin="anonymous">
<link rel="preload" href="/theme/fonts/Lato-Italic.ttf" as="font" type="font/ttf" crossorigin="anonymous">
<link rel="preload" href="/theme/fonts/Lato-Regular.ttf" as="font" type="font/ttf" crossorigin="anonymous">
<link rel="icon" href="favicon.svg">
<style>
{% include 'style.css' %}
{% include 'fonts.css' %}
</style>
<script defer src="/theme/instant-page.js"></script>
</head>
<body>
<div class="container">
<div class="bar">
{% block info %}
{% endblock %}
</div>
{% block content %}
{% endblock %}
</div>
<p class="copyright">
© 20122021 Tanner Collin
</p>
</body>
</html>

View File

@@ -0,0 +1,59 @@
@font-face {
font-family: 'Apparatus SIL';
src: url('/theme/fonts/AppSILR.ttf') format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Apparatus SIL';
font-style: italic;
src: url('/theme/fonts/AppSILI.ttf') format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Apparatus SIL';
font-weight: bold;
src: url('/theme/fonts/AppSILB.ttf') format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Apparatus SIL';
font-weight: bold;
font-style: italic;
src: url('/theme/fonts/AppSILBI.ttf') format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Lato';
font-style: italic;
font-weight: 400;
src: local('Lato Italic'), local('Lato-Italic'), url('/theme/fonts/Lato-Italic.ttf') format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Lato';
font-style: italic;
font-weight: 700;
src: local('Lato Bold Italic'), local('Lato-BoldItalic'), url('/theme/fonts/Lato-BoldItalic.ttf') format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
src: local('Lato Regular'), local('Lato-Regular'), url('/theme/fonts/Lato-Regular.ttf') format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: local('Lato Bold'), local('Lato-Bold'), url('/theme/fonts/Lato-Bold.ttf') format('truetype');
font-display: swap;
}

View File

@@ -0,0 +1,122 @@
{% extends "base.html" %}
{% block head %}
{{ super() }}
<meta name="description" content="The personal website of Tanner Collin." />
<meta name="summary" content="The personal website of Tanner Collin." />
{% endblock %}
{% block info %}
<img src="/theme/me.jpg" width="128" class="me" alt="me smiling wide and looking into the camera lit up brightly" />
<div class="info">
<h1>Tanner Collin</h1>
<p class="contact-icons">
<a href="mailto:site2@tannercollin.com" rel="noreferrer noopener"><img alt="email" src="/theme/mail.svg" width="20" height="20" /></a>
<a href="https://t.me/tannercollin" target="_blank" rel="noreferrer noopener"><img alt="telegram" src="/theme/telegram.svg" width="20" height="20" /></a>
<a href="https://github.com/tannercollin" target="_blank" rel="noreferrer noopener"><img alt="github" src="/theme/github.svg" width="20" height="20" /></a>
</p>
</div>
{% endblock %}
{% block content %}
<div class="content content-index">
<p>
Hi, I'm Tanner! I do firmware and web development in Calgary.
</p>
<div class="leftcol">
<div class="inside">
<h2>Contact Info</h2>
<p>
Email: <a href="mailto:site2@tannercollin.com">site2@tannercollin.com</a> <br />
Telegram: <a href="https://t.me/tannercollin" target="_blank" rel="noreferrer noopener">@tannercollin</a>
</p>
<h2>Resume</h2>
<ul>
<li>Firmware Engineer at <a href="https://cabanablockchain.com" target="_blank" rel="noreferrer noopener">Cabana Blockchain</a>, 2018</li>
<li>Lead Hardware Engineer at <a href="https://criticalcontrol.com/" target="_blank" rel="noreferrer noopener">Critical Control</a>, 20162018</li>
<li>Electrical Engineer at <a href="https://www.opener.aero/" target="_blank" rel="noreferrer noopener">Opener Aero</a>, 20162016</li>
<li>Electrical Engineer Intern at <a href="https://www.pason.com/" target="_blank" rel="noreferrer noopener">Pason Systems</a>, 20142015</li>
<li>BSc. Electrical Engineering from University of Calgary</li>
</ul>
<h2>Projects</h2>
<p>
My main hobby is working on software projects. I typically design websites or
build tools that make my life easier.
</p>
<h3>
<a href="https://news.t0.vc/" target="_blank" rel="noreferrer noopener">QotNews</a>
<a class="source" href="https://git.tannercollin.com/tanner/qotnews" target="_blank" rel="noreferrer noopener">source code</a>
</h3>
<div class="summary">
<p>Hacker News, Reddit, Lobsters, and Tildes articles pre-rendered in reader mode. Optimized for speed and distraction-free reading.</p>
</div>
<h3>
<a href="https://notica.us" target="_blank" rel="noreferrer noopener">Notica</a>
<a class="source" href="https://github.com/tannercollin/Notica" target="_blank" rel="noreferrer noopener">source code</a>
</h3>
<div class="summary">
<p>Send browser notifications from your terminal. No installation. No registration.</p>
</div>
<h3>
<a href="https://my.protospace.ca" target="_blank" rel="noreferrer noopener">Spaceport</a>
<a class="source" href="https://github.com/Protospace/spaceport" target="_blank" rel="noreferrer noopener">source code</a>
</h3>
<div class="summary">
<p>Makerspace members' portal for Calgary Protospace. It tracks membership, courses, training, access cards, and more.</p>
</div>
<h3>
<a href="https://txt.t0.vc" target="_blank" rel="noreferrer noopener">t0txt</a>
<a class="source" href="https://github.com/tannercollin/t0txt" target="_blank" rel="noreferrer noopener">source code</a>
</h3>
<div class="summary">
<p>Minimal command line pastebin. Allows you to upload text notes from a bash pipe or web browser.</p>
</div>
<h3>
<a href="https://github.com/tannercollin/standardnotes-fs" target="_blank" rel="noreferrer noopener">standardnotes-fs</a>
</h3>
<div class="summary">
<p>A filesystem that mounts your Standard Notes account as a directory of text files that you can edit.</p>
</div>
</div>
</div>
<div class="rightcol">
<div class="inside">
<h2>Creations</h2>
<p>Sometimes I create art or interactive tech.</p>
{% for article in articles_page.object_list if article.category.name == 'Projects' %}
<h3><a href="{{ article.url }}">{{ article.title }}</a></h3>
<div class="summary">
{{ article.summary }}
</div>
{% endfor %}
<h2>Writing</h2>
<p>Various articles, mostly about computers.</p>
{% for article in articles_page.object_list if article.category.name == 'Writing' %}
<h3><a href="{{ article.url }}">{{ article.title }}</a></h3>
<div class="summary">
{{ article.summary }}
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,271 @@
html {
overflow-y: scroll;
}
body {
text-rendering: optimizeLegibility;
font: 1.2rem/1.0 Lato,sans-serif;
}
a {
text-decoration: none;
outline: none;
}
.info h1 {
font: 1.2rem/1.0 Lato,sans-serif;
}
.source {
font: 1rem/1.5 Apparatus SIL,serif;
}
pre {
font-size: 1rem;
padding: 1rem;
overflow-x: auto;
}
:not(pre)>code {
padding: 0 2px;
font-size: 0.9rem;
}
.container {
max-width: 56rem;
margin: 2rem auto 12rem auto;
}
.copyright {
font: 1rem/1.5 Apparatus SIL,serif;
text-align: center;
}
.toc {
float: right;
padding: 0.75rem;
padding-top: 0;
margin-left: 0.75rem;
}
.toc ul {
padding-left: 1.2rem;
margin: 0;
}
.toc li {
margin-top: 0.75rem;
}
.content p.metadata {
font: 1rem/1.0 Apparatus SIL,serif;
font-style: italic;
}
.content div.summary p {
margin-top: -0.5rem;
margin-left: 1rem;
font-style: italic;
}
.content {
max-width: 36rem;
}
.content-wide {
max-width: 46rem;;
}
.content-index {
max-width: none;
}
.content-index h3 {
font: 1.2rem/1.5 Apparatus SIL,serif;
}
.content p {
font: 1.2rem/1.5 Apparatus SIL,serif;
}
.content ul {
padding-left: 1.1rem;
}
.content li {
font: 1.2rem/1.5 Apparatus SIL,serif;
}
.content img {
width: 100%;
height: auto;
}
.content .aside {
display: inline;
float: left;
position: relative;
width: 8rem;
margin-left: -9rem;
font-size: 1rem;
}
.toclink:not(:hover) {
border-bottom: none;
}
@media screen and (min-width:36rem) {
.content {
margin-left: 10rem;
}
.bar {
margin-top: 2px;
float: left;
width: 8rem;
text-align: right;
}
.bar .me {
width: 100%;
display: block;
}
.bar .info {
margin-top: 1.75rem;
}
.bar .contact-icons a {
border-bottom: none;
}
.bar .contact-icons img {
width: 1.25rem;
height: 1.25rem;
margin-left: 0.5rem;
}
}
@media screen and (max-width:36rem) {
.bar {
display: table;
overflow: auto;
margin: auto;
margin-top: -0.5rem;
}
.bar .me {
float: left;
height: 6.5rem;
width: auto;
}
.bar .info {
float: left;
margin-top: 0.5rem;
margin-left: 1.5rem;
}
.bar .contact-icons {
margin-top: 1rem;
margin-left: 0.25rem;
margin-bottom: 0;
}
.bar .contact-icons a {
border-bottom: none;
}
.bar .contact-icons img {
width: 1.25rem;
height: 1.25rem;
margin-right: 1rem;
}
}
@media screen and (min-width:58rem) {
.container {
max-width: 75rem;
}
.leftcol {
float: left;
width: 50%;
margin-top: -1rem;
}
.leftcol > .inside {
padding-right: 1rem;
}
.rightcol {
overflow: hidden;
margin-top: -1rem;
}
.rightcol > .inside {
padding-left: 1rem;
}
}
body {
background-color: #eee;
color: #000;
}
a {
color: #000;
border-bottom: 1px solid #000;
}
pre {
background-color: #ddd;
}
:not(pre)>code {
background-color: #ddd;
}
.toc {
background-color: #ddd;
}
.content p.metadata {
color: #555;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
color: #eee;
}
a {
color: #eee;
border-bottom: 1px solid #eee;
}
pre {
background-color: #222;
}
:not(pre)>code {
background-color: #222;
}
.toc {
background-color: #222;
}
.content p.metadata {
color: #aaa;
}
.content img {
filter: brightness(75%);
}
.contact-icons img {
filter: invert(1);
}
}

7647
yarn.lock

File diff suppressed because it is too large Load Diff