Compare commits
	
		
			1 commit
		
	
	
		
			main
			...
			activitypu
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 46725178d2 | 
							
								
								
									
										3
									
								
								.gitattributes
									
										
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
				
			
			@ -1,5 +1,2 @@
 | 
			
		|||
*.jpg filter=lfs diff=lfs merge=lfs -text
 | 
			
		||||
*.jpeg filter=lfs diff=lfs merge=lfs -text
 | 
			
		||||
*.pxm filter=lfs diff=lfs merge=lfs -text
 | 
			
		||||
*.mov filter=lfs diff=lfs merge=lfs -text
 | 
			
		||||
*.mp4 filter=lfs diff=lfs merge=lfs -text
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										6
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
				
			
			@ -1,12 +1,6 @@
 | 
			
		|||
LightroomExports/
 | 
			
		||||
node_modules/
 | 
			
		||||
public/
 | 
			
		||||
/documentation/mirrors/
 | 
			
		||||
/resources/
 | 
			
		||||
.hugo_build.lock
 | 
			
		||||
*.log
 | 
			
		||||
*.orig
 | 
			
		||||
*~
 | 
			
		||||
 | 
			
		||||
# Backup files for Markdown files processed in-place with sed
 | 
			
		||||
*.md-e
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										18
									
								
								.gitmodules
									
										
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
				
			
			@ -1,18 +0,0 @@
 | 
			
		|||
[submodule "themes/platters"]
 | 
			
		||||
	path = themes/platters
 | 
			
		||||
	url = nutmeg:git/hugo-theme-platters.git
 | 
			
		||||
[submodule "themes/termlite"]
 | 
			
		||||
	path = themes/termlite
 | 
			
		||||
	url = git@github.com:erynofwales/hugo-theme-termlite.git
 | 
			
		||||
[submodule "themes/resource-builders"]
 | 
			
		||||
	path = themes/resource-builders
 | 
			
		||||
	url = git@github.com:erynofwales/hugo-resource-builders.git
 | 
			
		||||
[submodule "themes/image-utils"]
 | 
			
		||||
	path = themes/image-utils
 | 
			
		||||
	url = git@github.com:erynofwales/hugo-image-utilities.git
 | 
			
		||||
[submodule "themes/photostream"]
 | 
			
		||||
	path = themes/photostream
 | 
			
		||||
	url = git@github.com:erynofwales/hugo-theme-photostream.git
 | 
			
		||||
[submodule "themes/feeds"]
 | 
			
		||||
	path = themes/feeds
 | 
			
		||||
	url = git@github.com:erynofwales/hugo-theme-feeds.git
 | 
			
		||||
| 
						 | 
				
			
			@ -1,15 +0,0 @@
 | 
			
		|||
snippet jp "lang jp shortcode" w
 | 
			
		||||
{{< lang jp >}}$1{{< /lang >}}
 | 
			
		||||
endsnippet
 | 
			
		||||
 | 
			
		||||
snippet jpp "lang jp shortcode with expansion" w
 | 
			
		||||
{{% lang jp %}}$1{{% /lang %}}
 | 
			
		||||
endsnippet
 | 
			
		||||
 | 
			
		||||
snippet tess "tess shortcode" w
 | 
			
		||||
{{< tess >}}
 | 
			
		||||
endsnippet
 | 
			
		||||
 | 
			
		||||
snippet ruby "ruby shortcode" w
 | 
			
		||||
{{< ruby "$1" >}}$2{{< /ruby >}}
 | 
			
		||||
endsnippet
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
-- Eryn Wells <eryn@erynwells.me>
 | 
			
		||||
 | 
			
		||||
vim.bo.shiftwidth = 2
 | 
			
		||||
vim.bo.softtabstop = 2
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
-- Eryn Wells <eryn@erynwells.me>
 | 
			
		||||
 | 
			
		||||
local root = gitTopLevelDirectory()
 | 
			
		||||
vim.opt_local.path:prepend(root .. "/assets/scripts/**")
 | 
			
		||||
vim.opt_local.path:prepend(root .. "/assets/styles/**")
 | 
			
		||||
vim.opt_local.path:prepend(root .. "/layouts/**")
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
-- Eryn Wells <eryn@erynwells.me>
 | 
			
		||||
 | 
			
		||||
local root = gitTopLevelDirectory()
 | 
			
		||||
vim.opt_local.path:prepend(root .. "/assets/scripts/**")
 | 
			
		||||
| 
						 | 
				
			
			@ -1,11 +0,0 @@
 | 
			
		|||
-- Eryn Wells <eryn@erynwells.me>
 | 
			
		||||
 | 
			
		||||
vim.bo.textwidth = 80
 | 
			
		||||
 | 
			
		||||
vim.cmd [[
 | 
			
		||||
    iabbrev tokyo Tōkyō
 | 
			
		||||
    iabbrev Tokyo Tōkyō
 | 
			
		||||
    iabbrev kyoto Kyōto
 | 
			
		||||
    iabbrev Kyoto Kyōto
 | 
			
		||||
    iabbrev xx ×
 | 
			
		||||
]]
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +0,0 @@
 | 
			
		|||
-- Eryn Wells <eryn@erynwells.me>
 | 
			
		||||
 | 
			
		||||
local filetypedetectGroup = vim.api.nvim_create_augroup("HugoHTMLTemplates", {clear = true})
 | 
			
		||||
vim.api.nvim_create_autocmd({"BufRead", "BufNewFile"}, {
 | 
			
		||||
    pattern = {"**/layouts/**/*.html"},
 | 
			
		||||
    group = filetypedetectGroup,
 | 
			
		||||
    command = "set ft=gohtmltmpl",
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										28
									
								
								Makefile
									
										
									
									
									
								
							
							
						
						| 
						 | 
				
			
			@ -1,9 +1,5 @@
 | 
			
		|||
# Eryn Wells <eryn@erynwells.me>
 | 
			
		||||
 | 
			
		||||
BUILD_DIR=public
 | 
			
		||||
 | 
			
		||||
CONTENT_PATH=content
 | 
			
		||||
 | 
			
		||||
DEPLOY_USER=eryn
 | 
			
		||||
DEPLOY_HOSTNAME=nutmeg.erynwells.me
 | 
			
		||||
DEPLOY_PATH=/srv/www/erynwells.me/html
 | 
			
		||||
| 
						 | 
				
			
			@ -11,34 +7,24 @@ DEPLOY_LOCATION=$(DEPLOY_USER)@$(DEPLOY_HOSTNAME):$(DEPLOY_PATH)
 | 
			
		|||
 | 
			
		||||
HOSTNAME=$(shell hostname -s)
 | 
			
		||||
 | 
			
		||||
NETHACK_LOGFILE=$(shell command nethack --showpaths | grep scoredir | sed 's/.*"\(.*\)".*/\1/g')/logfile
 | 
			
		||||
NETHACK_LOGFILE=$(shell nethack --showpaths | grep scoredir | sed 's/.*"\(.*\)".*/\1/g')/logfile
 | 
			
		||||
NETHACK_LOGFILE_DATA_FILE=data/nethack/logfile/$(HOSTNAME).json
 | 
			
		||||
 | 
			
		||||
.PHONY: site deploy clean
 | 
			
		||||
.PHONY: deploy clean
 | 
			
		||||
 | 
			
		||||
site:
 | 
			
		||||
site: public/index.html nethack
 | 
			
		||||
	@echo "Building site"
 | 
			
		||||
	hugo --buildFuture --enableGitInfo --destination "$(BUILD_DIR)"
 | 
			
		||||
	hugo
 | 
			
		||||
 | 
			
		||||
deploy: site
 | 
			
		||||
	@echo "Removing .DS_Store files from $(DEPLOY_LOCATION)"
 | 
			
		||||
	find "$(BUILD_DIR)" -name .DS_Store -print -delete
 | 
			
		||||
	@echo "Deploying to $(DEPLOY_LOCATION)"
 | 
			
		||||
	rsync -avz --no-times --no-perms --delete "$(BUILD_DIR)/" "$(DEPLOY_LOCATION)"
 | 
			
		||||
	git tag -f $(shell ./scripts/website deployment next-tag)
 | 
			
		||||
	rsync -avz --no-times --no-perms --delete public/ $(DEPLOY_LOCATION)
 | 
			
		||||
 | 
			
		||||
deployall: nethack deploy
 | 
			
		||||
 | 
			
		||||
nethack: nethack-logfile nethack-commit
 | 
			
		||||
 | 
			
		||||
nethack-logfile: $(NETHACK_LOGFILE)
 | 
			
		||||
nethack: $(NETHACK_LOGFILE)
 | 
			
		||||
ifeq (,$(wildcard $<))
 | 
			
		||||
	@echo "Importing Nethack logfile from $(NETHACK_LOGFILE)"
 | 
			
		||||
	scripts/import-nethack-logfile.py -o $(NETHACK_LOGFILE_DATA_FILE) $<
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
nethack-commit: $(NETHACK_LOGFILE_DATA_FILE)
 | 
			
		||||
	if ! git diff --quiet $<; then git commit -m "Update Nethack logfile for $(HOSTNAME)" -- $<; fi
 | 
			
		||||
 | 
			
		||||
clean:
 | 
			
		||||
	rm -rf "$(BUILD_DIR)/"
 | 
			
		||||
	rm -rf public/
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										27
									
								
								activitypub/private.pem
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,27 @@
 | 
			
		|||
-----BEGIN RSA PRIVATE KEY-----
 | 
			
		||||
MIIEpAIBAAKCAQEAl1vLbuE1C76GKX90VTJj3FaXyy7Ri57o7dKvrl8Bh+fk9wi/
 | 
			
		||||
uNWxuAA0rA70wihttNvPTXXiBpwbHdV9LQ/48hpmy2IJQWBvaOB+FXxDeUGelnwZ
 | 
			
		||||
zFYVH6ELmm+zEev6LUmkI8QtPakcw82LQV/7FeR5tZvAPHXqZmQATSsfKBKDPGje
 | 
			
		||||
aesNZ6P2g558DJnf35ksBYUXKCPnuKpJP31gAcpvSUPQGT2/wSPlH6T30U0taOhy
 | 
			
		||||
c3HUb2IqTKeZfe3EB2uliBGIGZnr6PL+B1OaXUehjT4M0F3RmsRRqpwnwXuFdekP
 | 
			
		||||
4CjWjgU2S8aC7wAVVhQl+pXzH2IxQyTrtoGW4QIDAQABAoIBAFOXw/P591UEJY6X
 | 
			
		||||
sMU47kQLowv5UIue+SAX4yUXnX3UyfTRZSmNA/kOTAjWvcDZmTVwzL4IJAvofWVt
 | 
			
		||||
uhOAJcp4YFtlSp1LyFwQ2DG/jnhSkGamJY1f4Yy1YfYqSKjWPjZy3G9QcngChTty
 | 
			
		||||
sr0paD/ADqQJYBNEQG/KPQxhi3SSY2VoKhnO8bn9sFFXN10kpYCVMm7ry2eLkjmN
 | 
			
		||||
z2qLqGYv0XJzWydNUGQ4mEakqCu4/4Yzv2lzV2eQvk7to9An9hmwI5RrTdxMod+h
 | 
			
		||||
tOKzH0YJ2ofShEhW8lBWiPpuIRwHWXn1ceWqqyyY2bHF6t4/uVYyrCI6fN9wuJfT
 | 
			
		||||
hESAiI0CgYEAyML/x0+zXqKbs+XojJ2eaC2ZYO525f+Xs744hAjAqzxdY9VKynS5
 | 
			
		||||
YcvguNfPd06AVqOZC/DOSc/SB3Rpcjad0iK9T3IGLZMs54o8nd7qSLrcmnGksua6
 | 
			
		||||
HSAvRMIjg7RQZKX97Q55u/+aZlSq2R686vu+SucCh4wR/el2f9L55jcCgYEAwQD/
 | 
			
		||||
yzuQwPYbhGvSi/6Wnnd+v/3z1FNS+og5u5egfG4soqV4NwqfrzFEo/498oWUHQQx
 | 
			
		||||
R7T028b6jBq8C0WPULwlJK28Srhe14prD494GBpoQE7ORrfUyR2BV+a3aHnjaApI
 | 
			
		||||
p1HzyfBMK8I8C472S0qcyynSjvJwMMzccYhiX6cCgYEAvB6M0lSKfSjdwYqzh9/C
 | 
			
		||||
KVhhGoUZAJeprRGXn5EcCNh9oiig9mi9tTf3kP0YOHFkrMqLhLbWdi+4XKfmjtFC
 | 
			
		||||
vT80KRdtVpNMRoIwHrZczl3ajRXkAIdW6ifYYxOPd8AkfMtVzyX7SABQy4aWuPCm
 | 
			
		||||
kYHnZARnSGAmE8kJ8ujcpa0CgYEAuMCLG/HOFFjCaJWKO8aeSd1bcM/Za6PDp91l
 | 
			
		||||
XoS79MDGa394P0AlMeA4s785b4GdEdWkkt7PEd88kYc6AJQ6p28jIyRIPUKWTz/K
 | 
			
		||||
Ul/k9zW4G+8UGK/6ufZLjWhmo1UiccJ+X+TcOK+VSsIpFmtoqxQmWXiUfY/oA17n
 | 
			
		||||
mqqSeLsCgYBqujxgBzTEWtU5g/MPHORVjXgZ83NWOA+3LOoicho/iaRAhz7mI13Y
 | 
			
		||||
d7J7s6CSqlxtCXsfVthzXYF+u5eYblkqnOLIY7X+tfcsx00twQflfRkdwjdChZge
 | 
			
		||||
cQk1BW4MWNhsH92iXGmb3B5iktFupXPzLlspAaqKeSRq4+5FPfq7Yg==
 | 
			
		||||
-----END RSA PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										9
									
								
								activitypub/public.pem
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
-----BEGIN PUBLIC KEY-----
 | 
			
		||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl1vLbuE1C76GKX90VTJj
 | 
			
		||||
3FaXyy7Ri57o7dKvrl8Bh+fk9wi/uNWxuAA0rA70wihttNvPTXXiBpwbHdV9LQ/4
 | 
			
		||||
8hpmy2IJQWBvaOB+FXxDeUGelnwZzFYVH6ELmm+zEev6LUmkI8QtPakcw82LQV/7
 | 
			
		||||
FeR5tZvAPHXqZmQATSsfKBKDPGjeaesNZ6P2g558DJnf35ksBYUXKCPnuKpJP31g
 | 
			
		||||
AcpvSUPQGT2/wSPlH6T30U0taOhyc3HUb2IqTKeZfe3EB2uliBGIGZnr6PL+B1Oa
 | 
			
		||||
XUehjT4M0F3RmsRRqpwnwXuFdekP4CjWjgU2S8aC7wAVVhQl+pXzH2IxQyTrtoGW
 | 
			
		||||
4QIDAQAB
 | 
			
		||||
-----END PUBLIC KEY-----
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "{{ replace .Name "-" " " | title }}"
 | 
			
		||||
slug: link-{{ .Name }}
 | 
			
		||||
date: {{ .Date }}
 | 
			
		||||
categories: links
 | 
			
		||||
draft: true
 | 
			
		||||
tags: []
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Notes on {{ time.Now.Format "2006" }}W%%WEEK_NUMBER%%"
 | 
			
		||||
slug: weeknotes-{{ time.Now.Format "2006" }}w%%WEEK_NUMBER%%
 | 
			
		||||
date: {{ .Date | time.Format "2006-01-02" }}
 | 
			
		||||
categories: weeknotes
 | 
			
		||||
tags:
 | 
			
		||||
  - Weeknotes
 | 
			
		||||
draft: true
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1,18 +0,0 @@
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/************************
 | 
			
		||||
 * PARAGRAPH-SPACED LIST
 | 
			
		||||
 ************************/
 | 
			
		||||
 | 
			
		||||
p + .paragraph-spaced-list {
 | 
			
		||||
  margin-block-start: var(--space-paragraph);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.paragraph-spaced-list {
 | 
			
		||||
  li + li {
 | 
			
		||||
    margin-block-start: var(--space-paragraph);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,42 +0,0 @@
 | 
			
		|||
.home-latest {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-column: main-start / main-end;
 | 
			
		||||
  grid-template-columns: 1fr 1fr 1fr 1fr;
 | 
			
		||||
  grid-template-rows: min-content min-content;
 | 
			
		||||
  grid-template-areas:
 | 
			
		||||
    "blog1 blog1 blog2 blog2"
 | 
			
		||||
    "photo1 photo2 photo3 photo4";
 | 
			
		||||
 | 
			
		||||
  .home-latest__blog {
 | 
			
		||||
    margin-block-end: var(--space-m);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .home-latest__blog:nth-of-type(1) {
 | 
			
		||||
    grid-area: blog1;
 | 
			
		||||
    border-right: 2px dashed var(--gray6);
 | 
			
		||||
    padding-inline-end: var(--space-s);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .home-latest__blog:nth-of-type(2) {
 | 
			
		||||
    grid-area: blog2;
 | 
			
		||||
    padding-inline-start: var(--space-s);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .home-latest__photo {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: 480px) {
 | 
			
		||||
  .home-latest {
 | 
			
		||||
    grid-template-columns: 1fr 1fr;
 | 
			
		||||
    grid-template-rows: repeat(min-content, 4);
 | 
			
		||||
    grid-template-areas:
 | 
			
		||||
      "blog1 blog2"
 | 
			
		||||
      "photo1 photo2"
 | 
			
		||||
      "photo3 photo4";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
p + .home-latest {
 | 
			
		||||
  margin-block-start: var(--space-paragraph);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,100 +0,0 @@
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/******************
 | 
			
		||||
 * NETHACK LOGFILE
 | 
			
		||||
 ******************/
 | 
			
		||||
 | 
			
		||||
#dungeon-background {
 | 
			
		||||
  width: 100vw;
 | 
			
		||||
  height: 100vh;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  z-index: -1;
 | 
			
		||||
  filter: brightness(0.3);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nethack-logfile {
 | 
			
		||||
  margin-inline-start: 0;
 | 
			
		||||
  padding-inline-start: 0;
 | 
			
		||||
 | 
			
		||||
  .nethack-logentry {
 | 
			
		||||
    align-items: first baseline;
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-columns: min-content min-content auto min-content;
 | 
			
		||||
    grid-template-areas:
 | 
			
		||||
      "list-marker entry-marker entry-date        entry-character-descriptor"
 | 
			
		||||
      ".           .            entry-description entry-description"
 | 
			
		||||
      ".           .            entry-stats       entry-stats";
 | 
			
		||||
    gap: var(--space-xs);
 | 
			
		||||
    margin-inline-start: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.nethack-logentry {
 | 
			
		||||
  &:not(:last-child) {
 | 
			
		||||
    margin-block-end: var(--space-l);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &::before {
 | 
			
		||||
    grid-area: list-marker;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .nethack-logentry__marker {
 | 
			
		||||
    grid-area: entry-marker;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .nethack-logentry__date {
 | 
			
		||||
    grid-area: entry-date;
 | 
			
		||||
    line-height: 1;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .nethack-logentry__character-descriptor {
 | 
			
		||||
    font-family: var(--font-family-monospace);
 | 
			
		||||
    font-size: var(--text-s);
 | 
			
		||||
    grid-area: entry-character-descriptor;
 | 
			
		||||
    line-height: 1;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .nethack-logentry__description {
 | 
			
		||||
    grid-area: entry-description;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .nethack-logentry__stats {
 | 
			
		||||
    border: 0;
 | 
			
		||||
    color: var(--text-color-secondary);
 | 
			
		||||
    font-family: var(--font-family-monospace);
 | 
			
		||||
    font-size: var(--text-s);
 | 
			
		||||
    grid-area: entry-stats;
 | 
			
		||||
    margin-block: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    -webkit-border-horizontal-spacing: 0;
 | 
			
		||||
    -webkit-border-vertical-spacing: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .nethack-logentry__stats {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    text-transform: uppercase;
 | 
			
		||||
    vertical-align: bottom;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
 | 
			
		||||
    thead {
 | 
			
		||||
      font-weight: bolder;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .nethack-logentry__score,
 | 
			
		||||
    .nethack-logentry__hp,
 | 
			
		||||
    .nethack-logentry__level {
 | 
			
		||||
      width: 16rem;
 | 
			
		||||
      text-align: right;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 464 KiB  | 
| 
						 | 
				
			
			@ -1,758 +0,0 @@
 | 
			
		|||
const NUMBER_OF_ROOMS = 12;
 | 
			
		||||
const TUNNEL_PASSES = 3;
 | 
			
		||||
 | 
			
		||||
class Cell {
 | 
			
		||||
  static CORRIDOR = "#";
 | 
			
		||||
  static DOOR_CLOSED = "+";
 | 
			
		||||
 | 
			
		||||
  character;
 | 
			
		||||
  characterColor;
 | 
			
		||||
  backgroundColor;
 | 
			
		||||
 | 
			
		||||
  constructor(char, charColor) {
 | 
			
		||||
    this.character = char;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  empty() { this.character = " "; }
 | 
			
		||||
  floor() { this.character = "."; }
 | 
			
		||||
  upStair() { this.character = "<"; }
 | 
			
		||||
  downStair() { this.character = ">"; }
 | 
			
		||||
  corridor() { this.character = Cell.CORRIDOR; }
 | 
			
		||||
  topLeftWall() { this.character = "┌"; }
 | 
			
		||||
  topRightWall() { this.character = "┐"; }
 | 
			
		||||
  bottomLeftWall() { this.character = "└"; }
 | 
			
		||||
  bottomRightWall() { this.character = "┘"; }
 | 
			
		||||
  horizontalWall() { this.character = "─"; }
 | 
			
		||||
  verticalWall() { this.character = "│"; }
 | 
			
		||||
  doorClosed() { this.character = Cell.DOOR_CLOSED; }
 | 
			
		||||
 | 
			
		||||
  isEmpty() { return !this.character || this.character === " "; }
 | 
			
		||||
  isDoor() { return this.character === Cell.DOOR_CLOSED; }
 | 
			
		||||
  isCorridor() { return this.character === Cell.CORRIDOR; }
 | 
			
		||||
 | 
			
		||||
  canBecomeDoor() { return this.character === "─" || this.character === "│" }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Point {
 | 
			
		||||
  x = 0;
 | 
			
		||||
  y = 0;
 | 
			
		||||
 | 
			
		||||
  constructor(x, y) {
 | 
			
		||||
    if (x) { this.x = x; }
 | 
			
		||||
    if (y) { this.y = y; }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  *neighbors() {
 | 
			
		||||
    const x = this.x;
 | 
			
		||||
    const y = this.y;
 | 
			
		||||
 | 
			
		||||
    yield new Point(x - 1, y - 1);
 | 
			
		||||
    yield new Point(x,     y - 1);
 | 
			
		||||
    yield new Point(x + 1, y - 1);
 | 
			
		||||
    yield new Point(x - 1, y);
 | 
			
		||||
    yield new Point(x + 1, y);
 | 
			
		||||
    yield new Point(x - 1, y + 1);
 | 
			
		||||
    yield new Point(x,     y + 1);
 | 
			
		||||
    yield new Point(x + 1, y + 1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  equalsPoint(other) { return this.x === other.x && this.y === other.y; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Size {
 | 
			
		||||
  width = 0;
 | 
			
		||||
  height = 0;
 | 
			
		||||
 | 
			
		||||
  constructor(width, height) {
 | 
			
		||||
    this.width = width;
 | 
			
		||||
    this.height = height;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Rect {
 | 
			
		||||
  origin = new Point();
 | 
			
		||||
  size = new Size();
 | 
			
		||||
 | 
			
		||||
  static fromCoordinates(x, y, w, h) {
 | 
			
		||||
    return new Rect(new Point(x, y), new Size(w, h));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constructor(origin, size) {
 | 
			
		||||
    this.origin = origin;
 | 
			
		||||
    this.size = size;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get minX() { return this.origin.x; }
 | 
			
		||||
  get minY() { return this.origin.y; }
 | 
			
		||||
  get maxX() { return this.origin.x + this.size.width; }
 | 
			
		||||
  get maxY() { return this.origin.y + this.size.height; }
 | 
			
		||||
 | 
			
		||||
  get width() { return this.size.width; }
 | 
			
		||||
  get height() { return this.size.height; }
 | 
			
		||||
  get area() { return this.size.width * this.size.height; }
 | 
			
		||||
 | 
			
		||||
  *xCoordinates() {
 | 
			
		||||
    for (let x = this.minX; x <= this.maxX; x++) {
 | 
			
		||||
      yield x;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  *yCoordinates() {
 | 
			
		||||
    for (let y = this.minY; y <= this.maxY; y++) {
 | 
			
		||||
      yield y;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  insetRect(inset) {
 | 
			
		||||
    const twiceInset = 2 * inset;
 | 
			
		||||
 | 
			
		||||
    return Rect.fromCoordinates(
 | 
			
		||||
      this.origin.x + inset,
 | 
			
		||||
      this.origin.y + inset,
 | 
			
		||||
      this.size.width - twiceInset,
 | 
			
		||||
      this.size.height - twiceInset
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  intersects(otherRect) {
 | 
			
		||||
    if (otherRect.minX > this.maxX)
 | 
			
		||||
      return false;
 | 
			
		||||
 | 
			
		||||
    if (otherRect.maxX < this.minX)
 | 
			
		||||
      return false;
 | 
			
		||||
 | 
			
		||||
    if (otherRect.minY > this.maxY)
 | 
			
		||||
      return false;
 | 
			
		||||
 | 
			
		||||
    if (otherRect.maxY < this.minY)
 | 
			
		||||
      return false;
 | 
			
		||||
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Grid {
 | 
			
		||||
  #size;
 | 
			
		||||
  #cells = [];
 | 
			
		||||
  #rooms = [];
 | 
			
		||||
 | 
			
		||||
  constructor(width, height) {
 | 
			
		||||
    this.#size = new Size(width, height);
 | 
			
		||||
 | 
			
		||||
    this.#cells = new Array(width * height);
 | 
			
		||||
    for (let i = 0; i < this.#cells.length; i++) {
 | 
			
		||||
      this.#cells[i] = new Cell(" ");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get bounds() { return new Rect(new Point(), this.#size); }
 | 
			
		||||
  get width() { return this.#size.width; }
 | 
			
		||||
  get height() { return this.#size.height; }
 | 
			
		||||
  get rooms() { return this.#rooms; }
 | 
			
		||||
 | 
			
		||||
  generate(p, roomGeneratorClass, tunnelGeneratorClass) {
 | 
			
		||||
    this.#generateRooms(p, new roomGeneratorClass(this, NUMBER_OF_ROOMS));
 | 
			
		||||
    this.#placeStairs();
 | 
			
		||||
    this.#digCorridors(p, new tunnelGeneratorClass(this, TUNNEL_PASSES));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #generateRooms(p, generator) {
 | 
			
		||||
    generator.generate(p);
 | 
			
		||||
    this.#rooms = generator.rooms;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #placeStairs() {
 | 
			
		||||
    const indexOfRoomWithUpStairs = randomInt(this.#rooms.length);
 | 
			
		||||
    const coordinateOfUpStair = this.#rooms[indexOfRoomWithUpStairs].randomPoint();
 | 
			
		||||
    this.cellAt(coordinateOfUpStair.x, coordinateOfUpStair.y).upStair();
 | 
			
		||||
 | 
			
		||||
    while (true) {
 | 
			
		||||
      let indexOfRoomForDownStair = randomInt(this.#rooms.length);
 | 
			
		||||
      if (indexOfRoomForDownStair == indexOfRoomWithUpStairs) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const coordinateOfDownStair = this.#rooms[indexOfRoomForDownStair].randomPoint();
 | 
			
		||||
      this.cellAt(coordinateOfDownStair.x, coordinateOfDownStair.y).downStair();
 | 
			
		||||
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #digCorridors(p, generator) {
 | 
			
		||||
    generator.generate(p);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pointIsInBounds(pt) { return pt.x >= 0 && pt.x < this.width && pt.y >= 0 && pt.y < this.height; }
 | 
			
		||||
 | 
			
		||||
  cellAt(x, y) { return this.#cells[y * this.width + x]; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Room {
 | 
			
		||||
  #rect;
 | 
			
		||||
 | 
			
		||||
  constructor(rect) {
 | 
			
		||||
    this.#rect = rect;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get bounds() { return this.#rect; }
 | 
			
		||||
 | 
			
		||||
  get minX() { return this.#rect.minX; }
 | 
			
		||||
  get minY() { return this.#rect.minY; }
 | 
			
		||||
  get maxX() { return this.#rect.maxX; }
 | 
			
		||||
  get maxY() { return this.#rect.maxY; }
 | 
			
		||||
 | 
			
		||||
  randomPoint() {
 | 
			
		||||
    return new Point(
 | 
			
		||||
      this.#rect.minX + 1 + randomInt(this.#rect.size.width - 2),
 | 
			
		||||
      this.#rect.minY + 1 + randomInt(this.#rect.size.height - 2)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  transformCellAt(pt, cell) {
 | 
			
		||||
    const minX = this.minX;
 | 
			
		||||
    const minY = this.minY;
 | 
			
		||||
    const maxX = this.maxX;
 | 
			
		||||
    const maxY = this.maxY;
 | 
			
		||||
 | 
			
		||||
    const x = pt.x;
 | 
			
		||||
    const y = pt.y;
 | 
			
		||||
 | 
			
		||||
    if (y === minY && x === minX) { cell.topLeftWall(); }
 | 
			
		||||
    else if (y === minY && x === maxX) { cell.topRightWall(); }
 | 
			
		||||
    else if (y === maxY && x === minX) { cell.bottomLeftWall(); }
 | 
			
		||||
    else if (y === maxY && x === maxX) { cell.bottomRightWall(); }
 | 
			
		||||
    else if (y === minY || y === maxY) { cell.horizontalWall(); }
 | 
			
		||||
    else if (x === minX || x === maxX) { cell.verticalWall(); }
 | 
			
		||||
    else if ((x > minX && x < maxX) && (y > minY && y < maxY)) { return cell.floor(); }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class NRandomRoomsGenerator {
 | 
			
		||||
  static MIN_ROOM_DIMENSION = 7;
 | 
			
		||||
  static MAX_ROOM_DIMENSION = 12;
 | 
			
		||||
 | 
			
		||||
  #numberOfRooms = 12;
 | 
			
		||||
  #rooms;
 | 
			
		||||
  #grid;
 | 
			
		||||
 | 
			
		||||
  constructor(grid, numberOfRooms) {
 | 
			
		||||
    this.#grid = grid;
 | 
			
		||||
 | 
			
		||||
    if (numberOfRooms) {
 | 
			
		||||
      this.#numberOfRooms = numberOfRooms;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get rooms() {
 | 
			
		||||
    if (!this.#rooms) {
 | 
			
		||||
      this.#generateRooms();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return this.#rooms;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get #bounds() { return this.#grid.bounds; }
 | 
			
		||||
 | 
			
		||||
  generate(p) {
 | 
			
		||||
    this.#generateRooms();
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < this.#rooms.length; i++) {
 | 
			
		||||
      let room = this.#rooms[i];
 | 
			
		||||
      for (let y = room.minY; y <= room.maxY; y++) {
 | 
			
		||||
        for (let x = room.minX; x <= room.maxX; x++) {
 | 
			
		||||
          let point = new Point(x, y);
 | 
			
		||||
          let cell = this.#grid.cellAt(x, y);
 | 
			
		||||
 | 
			
		||||
          room.transformCellAt(point, cell);
 | 
			
		||||
          cell.characterColor = p.color(255);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #generateRooms() {
 | 
			
		||||
    let rects = new Array();
 | 
			
		||||
 | 
			
		||||
    const sizeRange = NRandomRoomsGenerator.MAX_ROOM_DIMENSION - NRandomRoomsGenerator.MIN_ROOM_DIMENSION;
 | 
			
		||||
 | 
			
		||||
    while (rects.length < this.#numberOfRooms) {
 | 
			
		||||
      const randomSize = new Size(
 | 
			
		||||
        NRandomRoomsGenerator.MIN_ROOM_DIMENSION + randomInt(sizeRange),
 | 
			
		||||
        NRandomRoomsGenerator.MIN_ROOM_DIMENSION + randomInt(sizeRange)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const randomOrigin = new Point(
 | 
			
		||||
        this.#bounds.minX + randomInt(this.#bounds.maxX - randomSize.width),
 | 
			
		||||
        this.#bounds.minY + randomInt(this.#bounds.maxY - randomSize.height)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const proposedRoomRect = new Rect(randomOrigin, randomSize);
 | 
			
		||||
 | 
			
		||||
      // Check that the rect doesn't intersect with any other rects.
 | 
			
		||||
      if (rects.some(e => e.intersects(proposedRoomRect))) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      rects.push(proposedRoomRect);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    rects.sort((a, b) => {
 | 
			
		||||
      if (a.origin.x < b.origin.x) { return -1; }
 | 
			
		||||
      if (a.origin.x > b.origin.x) { return 1; }
 | 
			
		||||
      if (a.origin.y < b.origin.y) { return -1; }
 | 
			
		||||
      if (a.origin.y > b.origin.y) { return 1; }
 | 
			
		||||
      return 0;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.#rooms = rects.map(r => new Room(r.insetRect(1)));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class TunnelGenerator {
 | 
			
		||||
  static MAX_DOORS = 100;
 | 
			
		||||
 | 
			
		||||
  #grid;
 | 
			
		||||
  #passes = 3;
 | 
			
		||||
  #numberOfDoorsPlaced = 0;
 | 
			
		||||
 | 
			
		||||
  constructor(grid, passes) {
 | 
			
		||||
    this.#grid = grid;
 | 
			
		||||
    if (passes) { this.#passes = passes; }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  generate(p) {
 | 
			
		||||
    console.group("Digging tunnels");
 | 
			
		||||
 | 
			
		||||
    if (this.#passes >= 1) {
 | 
			
		||||
      this.#doPassOne(p);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.#passes >= 2) {
 | 
			
		||||
      this.#doPassTwo(p);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.#passes >= 3) {
 | 
			
		||||
      this.#doPassThree(p);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.groupEnd();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #doPassOne(p) {
 | 
			
		||||
    console.group("Pass 1");
 | 
			
		||||
    this.#iterateAndConnectRoomsWithOffset(p, 1);
 | 
			
		||||
    console.groupEnd();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #doPassTwo(p) {
 | 
			
		||||
    console.group("Pass 2");
 | 
			
		||||
    this.#iterateAndConnectRoomsWithOffset(p, 2);
 | 
			
		||||
    console.groupEnd();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #doPassThree(p) {
 | 
			
		||||
    console.group("Pass 3");
 | 
			
		||||
    this.#iterateAndConnectRoomsWithOffset(p, 3);
 | 
			
		||||
    console.groupEnd();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #iterateAndConnectRoomsWithOffset(p, offset) {
 | 
			
		||||
    let rooms = this.#grid.rooms;
 | 
			
		||||
    const numberOfRooms = rooms.length;
 | 
			
		||||
    for (let i = 0; i < numberOfRooms - offset; i++) {
 | 
			
		||||
      let fromRoom = rooms[i];
 | 
			
		||||
      let toRoom = rooms[i + offset];
 | 
			
		||||
 | 
			
		||||
      let [fromPoint, toPoint] = this.#findPointFromRoomToRoom(fromRoom, toRoom);
 | 
			
		||||
      if (!fromPoint || !toPoint) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (let neighbor of fromPoint.neighbors()) {
 | 
			
		||||
        if (!this.#grid.pointIsInBounds(neighbor)) {
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let cell = this.#grid.cellAt(neighbor.x, neighbor.y);
 | 
			
		||||
        if ((neighbor.x === fromPoint.x || neighbor.y === fromPoint.y) && cell.canBecomeDoor()) {
 | 
			
		||||
          cell.doorClosed();
 | 
			
		||||
          this.#numberOfDoorsPlaced++;
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const successfullyFoundTargetPoint = this.#digCorridorFromPointToPoint(p, fromPoint, toPoint);
 | 
			
		||||
 | 
			
		||||
      if (successfullyFoundTargetPoint) {
 | 
			
		||||
        for (let neighbor of toPoint.neighbors()) {
 | 
			
		||||
          if (!this.#grid.pointIsInBounds(neighbor)) {
 | 
			
		||||
            continue;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          let cell = this.#grid.cellAt(neighbor.x, neighbor.y);
 | 
			
		||||
          if ((neighbor.x === toPoint.x || neighbor.y === toPoint.y) && cell.canBecomeDoor()) {
 | 
			
		||||
            cell.doorClosed();
 | 
			
		||||
            this.#numberOfDoorsPlaced++;
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Dig a corridor from fromPoint to toPoint, assuming that both points are
 | 
			
		||||
   * adjacent to valid locations for doors on the map.
 | 
			
		||||
   *
 | 
			
		||||
   * This is as close a copy of dig_corridor in the Nethack source as I could
 | 
			
		||||
   * muster. It's not exactly pretty. This method assumed
 | 
			
		||||
   */
 | 
			
		||||
  #digCorridorFromPointToPoint(p, fromPoint, toPoint) {
 | 
			
		||||
    const MAX_STEPS = 500;
 | 
			
		||||
 | 
			
		||||
    if (!fromPoint || !toPoint) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const fromX = fromPoint.x;
 | 
			
		||||
    const toX = toPoint.x;
 | 
			
		||||
    const fromY = fromPoint.y;
 | 
			
		||||
    const toY = toPoint.y;
 | 
			
		||||
 | 
			
		||||
    let curX = fromX;
 | 
			
		||||
    let curY = fromY;
 | 
			
		||||
 | 
			
		||||
    let dx = 0;
 | 
			
		||||
    let dy = 0;
 | 
			
		||||
 | 
			
		||||
    // Set up the initial direction of the dig.
 | 
			
		||||
    if (toX > curX) {
 | 
			
		||||
      dx = 1;
 | 
			
		||||
    } else if (toY > curY) {
 | 
			
		||||
      dy = 1;
 | 
			
		||||
    } else if (toX < curX) {
 | 
			
		||||
      dx = -1;
 | 
			
		||||
    } else {
 | 
			
		||||
      dy = -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    curX -= dx;
 | 
			
		||||
    curY -= dy;
 | 
			
		||||
 | 
			
		||||
    let steps = 0;
 | 
			
		||||
    while (curX !== toX || curY !== toY) {
 | 
			
		||||
      if (steps++ > MAX_STEPS) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      curX += dx;
 | 
			
		||||
      curY += dy;
 | 
			
		||||
 | 
			
		||||
      if (curX >= this.#grid.width - 1 || curX <= 0 || curY <= 0 || curY >= this.#grid.height - 1) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let cell = this.#grid.cellAt(curX, curY);
 | 
			
		||||
      if (cell.isEmpty()) {
 | 
			
		||||
        cell.corridor();
 | 
			
		||||
      } else if (!cell.isCorridor()) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let dix = Math.abs(curX - toX);
 | 
			
		||||
      let diy = Math.abs(curY - toY);
 | 
			
		||||
 | 
			
		||||
      if (dix > diy && diy) {
 | 
			
		||||
        const random = randomInt(dix - diy + 1);
 | 
			
		||||
        if (!random) {
 | 
			
		||||
          dix = 0;
 | 
			
		||||
        }
 | 
			
		||||
      } else if (diy > dix && dix) {
 | 
			
		||||
        const random = randomInt(dix - diy + 1);
 | 
			
		||||
        if (!random) {
 | 
			
		||||
          diy = 0;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (dy && dix > diy) {
 | 
			
		||||
        const ddx = curX > toX ? -1 : 1;
 | 
			
		||||
 | 
			
		||||
        let cell = this.#grid.cellAt(curX + ddx, curY);
 | 
			
		||||
        if (cell.isEmpty() || cell.isCorridor()) {
 | 
			
		||||
          dx = ddx;
 | 
			
		||||
          dy = 0;
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
      } else if (dx && diy > dix) {
 | 
			
		||||
        const ddy = curY > toY ? -1 : 1;
 | 
			
		||||
 | 
			
		||||
        let cell = this.#grid.cellAt(curX, curY + ddy);
 | 
			
		||||
        if (cell.isEmpty() || cell.isCorridor()) {
 | 
			
		||||
          dy = ddy;
 | 
			
		||||
          dx = 0;
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      cell = this.#grid.cellAt(curX + dx, curY + dy);
 | 
			
		||||
      if (cell.isEmpty() || cell.isCorridor()) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (dx) {
 | 
			
		||||
        dx = 0;
 | 
			
		||||
        dy = toY < curY ? -1 : 1;
 | 
			
		||||
      } else {
 | 
			
		||||
        dy = 0;
 | 
			
		||||
        dx = toX < curX ? -1 : 1;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      cell = this.#grid.cellAt(curX + dx, curY + dy);
 | 
			
		||||
      if (cell.isEmpty() || cell.isCorridor()) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      dy = -dy;
 | 
			
		||||
      dx = -dx;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #findPointFromRoomToRoom(fromRoom, toRoom) {
 | 
			
		||||
    const fromRoomBounds = fromRoom.bounds;
 | 
			
		||||
    const toRoomBounds = toRoom.bounds;
 | 
			
		||||
 | 
			
		||||
    let foundFromPoint = false;
 | 
			
		||||
    let foundToPoint = false;
 | 
			
		||||
 | 
			
		||||
    let fromPoint;
 | 
			
		||||
    let toPoint;
 | 
			
		||||
    if (fromRoomBounds.maxX < toRoomBounds.minX) {
 | 
			
		||||
      // fromRoom is farther left than toRoom
 | 
			
		||||
      
 | 
			
		||||
      fromPoint = new Point(fromRoomBounds.maxX, fromRoomBounds.minY + 1 + randomInt(fromRoomBounds.height - 2));
 | 
			
		||||
      foundFromPoint = this.#canPlaceDoorAt(fromPoint);
 | 
			
		||||
 | 
			
		||||
      for (let y of fromRoomBounds.yCoordinates()) {
 | 
			
		||||
        fromPoint.y = y;
 | 
			
		||||
        foundFromPoint = this.#canPlaceDoorAt(fromPoint);
 | 
			
		||||
        if (foundFromPoint) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!foundFromPoint) {
 | 
			
		||||
        return [];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      toPoint = new Point(toRoomBounds.minX, toRoomBounds.minY + 1 + randomInt(toRoomBounds.height - 2));
 | 
			
		||||
      foundToPoint = this.#canPlaceDoorAt(toPoint);
 | 
			
		||||
 | 
			
		||||
      for (let y of toRoomBounds.yCoordinates()) {
 | 
			
		||||
        toPoint.y = y;
 | 
			
		||||
        foundToPoint = this.#canPlaceDoorAt(toPoint);
 | 
			
		||||
        if (foundToPoint) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!foundToPoint) {
 | 
			
		||||
        return [];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      fromPoint.x += 1;
 | 
			
		||||
      toPoint.x -= 1;
 | 
			
		||||
    } else if (fromRoomBounds.minX > toRoomBounds.maxX) {
 | 
			
		||||
      // fromRoom is farther right than toRoomBounds.
 | 
			
		||||
 | 
			
		||||
      fromPoint = new Point(toRoomBounds.maxX, toRoomBounds.minY + 1 + randomInt(toRoomBounds.height - 2));
 | 
			
		||||
      foundFromPoint = this.#canPlaceDoorAt(fromPoint);
 | 
			
		||||
 | 
			
		||||
      for (let y of toRoomBounds.yCoordinates()) {
 | 
			
		||||
        fromPoint.y = y;
 | 
			
		||||
        foundFromPoint = this.#canPlaceDoorAt(fromPoint);
 | 
			
		||||
        if (foundFromPoint) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!foundFromPoint) {
 | 
			
		||||
        return [];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      toPoint = new Point(fromRoomBounds.minX, fromRoomBounds.minY + 1 + randomInt(fromRoomBounds.height - 2));
 | 
			
		||||
      foundToPoint = this.#canPlaceDoorAt(toPoint);
 | 
			
		||||
 | 
			
		||||
      for (let y of fromRoomBounds.yCoordinates()) {
 | 
			
		||||
        toPoint.y = y;
 | 
			
		||||
        foundToPoint = this.#canPlaceDoorAt(toPoint);
 | 
			
		||||
        if (foundToPoint) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!foundToPoint) {
 | 
			
		||||
        return [];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      fromPoint.x -= 1;
 | 
			
		||||
      toPoint.x += 1;
 | 
			
		||||
    } else if (fromRoomBounds.maxY < (toRoomBounds.minY - 1)) {
 | 
			
		||||
      // fromRoom is above toRoom
 | 
			
		||||
 | 
			
		||||
      fromPoint = new Point(fromRoomBounds.minX + 1 + randomInt(fromRoomBounds.width - 2), fromRoomBounds.maxY);
 | 
			
		||||
      foundFromPoint = this.#canPlaceDoorAt(fromPoint);
 | 
			
		||||
 | 
			
		||||
      for (let x of fromRoomBounds.xCoordinates()) {
 | 
			
		||||
        fromPoint.x = x;
 | 
			
		||||
        foundFromPoint = this.#canPlaceDoorAt(fromPoint);
 | 
			
		||||
        if (foundFromPoint) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!foundFromPoint) {
 | 
			
		||||
        return [];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      toPoint = new Point(toRoomBounds.minX + 1 + randomInt(toRoomBounds.width - 2), toRoomBounds.minY);
 | 
			
		||||
      foundToPoint = this.#canPlaceDoorAt(toPoint);
 | 
			
		||||
 | 
			
		||||
      for (let x of toRoomBounds.xCoordinates()) {
 | 
			
		||||
        toPoint.x = x;
 | 
			
		||||
        foundToPoint = this.#canPlaceDoorAt(toPoint);
 | 
			
		||||
        if (foundToPoint) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!foundToPoint) {
 | 
			
		||||
        return [];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      fromPoint.y += 1;
 | 
			
		||||
      toPoint.y -= 1;
 | 
			
		||||
    } else if (fromRoomBounds.minY > (toRoomBounds.maxY + 1)) {
 | 
			
		||||
      // fromRoom is below toRoom
 | 
			
		||||
 | 
			
		||||
      fromPoint = new Point(toRoomBounds.minX + 1 + randomInt(toRoomBounds.width - 2), toRoomBounds.maxY);
 | 
			
		||||
      foundFromPoint = this.#canPlaceDoorAt(fromPoint);
 | 
			
		||||
 | 
			
		||||
      for (let x of toRoomBounds.xCoordinates()) {
 | 
			
		||||
        fromPoint.x = x;
 | 
			
		||||
        foundFromPoint = this.#canPlaceDoorAt(fromPoint);
 | 
			
		||||
        if (foundFromPoint) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!foundFromPoint) {
 | 
			
		||||
        return [];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      toPoint = new Point(fromRoomBounds.minX + 1 + randomInt(fromRoomBounds.width - 2), fromRoomBounds.minY);
 | 
			
		||||
      foundToPoint = this.#canPlaceDoorAt(toPoint);
 | 
			
		||||
 | 
			
		||||
      for (let x of fromRoomBounds.xCoordinates()) {
 | 
			
		||||
        toPoint.x = x;
 | 
			
		||||
        foundToPoint = this.#canPlaceDoorAt(toPoint);
 | 
			
		||||
        if (foundToPoint) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!foundToPoint) {
 | 
			
		||||
        return [];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      fromPoint.y += 1;
 | 
			
		||||
      toPoint.y -= 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return [fromPoint, toPoint];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #canPlaceDoorAt(pt) {
 | 
			
		||||
    if (this.#numberOfDoorsPlaced > TunnelGenerator.MAX_DOORS) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this.#grid.cellAt(pt.x, pt.y).canBecomeDoor()) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (let neighbor of pt.neighbors()) {
 | 
			
		||||
      if (!this.#grid.pointIsInBounds(neighbor)) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let cell = this.#grid.cellAt(neighbor.x, neighbor.y);
 | 
			
		||||
      if (cell.isDoor()) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function randomInt(n) {
 | 
			
		||||
  max = Math.floor(n);
 | 
			
		||||
  return Math.floor(Math.random() * max);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let grid;
 | 
			
		||||
 | 
			
		||||
new p5(p => {
 | 
			
		||||
  const CELL_WIDTH = 20;
 | 
			
		||||
  const CELL_HEIGHT = Math.floor(CELL_WIDTH * 1.3);
 | 
			
		||||
 | 
			
		||||
  p.setup = () => {
 | 
			
		||||
    const container = document.querySelector('#dungeon-background');
 | 
			
		||||
    console.assert(container, "Missing #dungeon-background element");
 | 
			
		||||
 | 
			
		||||
    canvasWidth = parseFloat(getComputedStyle(container).width);
 | 
			
		||||
    canvasHeight = parseFloat(getComputedStyle(container).height);
 | 
			
		||||
 | 
			
		||||
    let canvas = p.createCanvas(canvasWidth, canvasHeight);
 | 
			
		||||
    canvas.canvas.removeAttribute('style');
 | 
			
		||||
    container.appendChild(canvas.canvas);
 | 
			
		||||
 | 
			
		||||
    p.pixelDensity(p.displayDensity());
 | 
			
		||||
    p.textFont("Courier");
 | 
			
		||||
 | 
			
		||||
    const gridBounds = Rect.fromCoordinates(
 | 
			
		||||
      0, 0,
 | 
			
		||||
      Math.max(80, Math.ceil(canvasWidth / CELL_WIDTH) - 1),
 | 
			
		||||
      Math.max(24, Math.ceil(canvasHeight / CELL_HEIGHT) - 1)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    console.log(`Generating grid with size ${gridBounds.size.width} x ${gridBounds.size.height}`);
 | 
			
		||||
 | 
			
		||||
    grid = new Grid(gridBounds.size.width, gridBounds.size.height);
 | 
			
		||||
    grid.generate(p, NRandomRoomsGenerator, TunnelGenerator);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  p.draw = () => {
 | 
			
		||||
    console.log("Drawing");
 | 
			
		||||
 | 
			
		||||
    p.textSize(CELL_HEIGHT);
 | 
			
		||||
 | 
			
		||||
    for (let y = 0; y < grid.height; y++) {
 | 
			
		||||
      for (let x = 0; x < grid.width; x++) {
 | 
			
		||||
        let cell = grid.cellAt(x, y);
 | 
			
		||||
 | 
			
		||||
        let fillColor = cell.characterColor ? cell.characterColor : p.color(255);
 | 
			
		||||
        p.fill(fillColor);
 | 
			
		||||
 | 
			
		||||
        p.textAlign(p.CENTER, p.CENTER);
 | 
			
		||||
        p.text(cell.character, x * CELL_WIDTH, y * CELL_HEIGHT, CELL_WIDTH, CELL_HEIGHT);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    p.noLoop();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
}, '#dungeon-background');
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,8 @@
 | 
			
		|||
import rr from "scripts/lib/railroad.js";
 | 
			
		||||
{{ with resources.Get "scripts/lib/railroad.js" | fingerprint "sha512" }}
 | 
			
		||||
import rr from "{{ .RelPermalink }}";
 | 
			
		||||
{{ else }}
 | 
			
		||||
  {{ errorf "Unable to get railroad.js resource" }}
 | 
			
		||||
{{ end }}
 | 
			
		||||
 | 
			
		||||
class RailroadDiagramManager {
 | 
			
		||||
    constructor() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,123 +0,0 @@
 | 
			
		|||
class RubiksCubeScrambler extends HTMLElement {
 | 
			
		||||
    static #RandomMoveHysteresisMaxLength = 2;
 | 
			
		||||
 | 
			
		||||
    #shadowRoot;
 | 
			
		||||
    #movesListElement;
 | 
			
		||||
 | 
			
		||||
    #numberOfMovesToGenerate = 25;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        this.#shadowRoot = this.attachShadow({ mode: "open" });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    scramble() {
 | 
			
		||||
        console.log("Randomizing Rubik's cube...");
 | 
			
		||||
 | 
			
		||||
        const movesList = this.#movesListElement;
 | 
			
		||||
 | 
			
		||||
        while (movesList.childElementCount > this.#numberOfMovesToGenerate) {
 | 
			
		||||
            movesList.removeChild(movesList.lastChild);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let randomMoveHysteresis = [];
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < this.#numberOfMovesToGenerate; i++) {
 | 
			
		||||
            const randomMove = this.#randomMove(randomMoveHysteresis);
 | 
			
		||||
 | 
			
		||||
            let moveItem;
 | 
			
		||||
            if (i < movesList.childElementCount) {
 | 
			
		||||
                moveItem = movesList.children[i];
 | 
			
		||||
            } else {
 | 
			
		||||
                moveItem = document.createElement("li");
 | 
			
		||||
                movesList.appendChild(moveItem);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            moveItem.classList.add("scrambler__move");
 | 
			
		||||
            moveItem.classList.remove("scrambler__move--start", "scrambler__move--end");
 | 
			
		||||
 | 
			
		||||
            if (randomMove.includes("2")) {
 | 
			
		||||
                moveItem.classList.add("scrambler__move--start");
 | 
			
		||||
            } else if (randomMove.includes("'")) {
 | 
			
		||||
                moveItem.classList.add("scrambler__move--end");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            moveItem.innerText = randomMove;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #randomMove(hysteresis) {
 | 
			
		||||
        const faces = "FBLRUD";
 | 
			
		||||
 | 
			
		||||
        let move;
 | 
			
		||||
        do {
 | 
			
		||||
            move = faces.charAt(Math.floor(Math.random() * faces.length));
 | 
			
		||||
        } while (hysteresis && hysteresis.includes(move));
 | 
			
		||||
 | 
			
		||||
        if (hysteresis) {
 | 
			
		||||
            hysteresis.unshift(move);
 | 
			
		||||
            while (hysteresis.length > RubiksCubeScrambler.#RandomMoveHysteresisMaxLength) {
 | 
			
		||||
                hysteresis.pop();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        const modifierFactor = Math.random();
 | 
			
		||||
        if (modifierFactor < 0.33333) {
 | 
			
		||||
            move = "2" + move;
 | 
			
		||||
        } else if (modifierFactor < 0.666666) {
 | 
			
		||||
            move = move + "'";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return move;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #removeAllMoves() {
 | 
			
		||||
        const element = this.#movesListElement;
 | 
			
		||||
        while (element.hasChildNodes()) {
 | 
			
		||||
            element.removeChild(element.lastChild);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // MARK: Custom Element
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        let template = document.getElementById("rubiks-cube-scrambler-template");
 | 
			
		||||
        console.assert(template, "Couldn't find RubiksCubeScrambler component template in the document");
 | 
			
		||||
 | 
			
		||||
        const shadowRoot = this.#shadowRoot;
 | 
			
		||||
        shadowRoot.appendChild(template.content.cloneNode(true));
 | 
			
		||||
 | 
			
		||||
        this.#movesListElement = shadowRoot.querySelector(".scrambler__move-list");
 | 
			
		||||
 | 
			
		||||
        shadowRoot
 | 
			
		||||
            .querySelector("button[name='scramble']")
 | 
			
		||||
            .addEventListener("click", () => this.scramble());
 | 
			
		||||
 | 
			
		||||
        const patternLengthInputElement = shadowRoot.querySelector(".scrambler__pattern-length > input");
 | 
			
		||||
        patternLengthInputElement.value = this.#numberOfMovesToGenerate;
 | 
			
		||||
        patternLengthInputElement.addEventListener("input", event => {
 | 
			
		||||
            try {
 | 
			
		||||
                const integerValue = parseInt(event.target.value);
 | 
			
		||||
                this.#numberOfMovesToGenerate = integerValue;
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                console.error("Non-integer value of pattern length field", e);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.scramble();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    attributeChangedCallback(name, oldValue, newValue) {
 | 
			
		||||
        console.debug("RubiksCubeScrambler attribute changed", name, oldValue, newValue);
 | 
			
		||||
        if (name === "count") {
 | 
			
		||||
            try {
 | 
			
		||||
                let newIntValue = parseInt(newValue);
 | 
			
		||||
                this.#numberOfMovesToGenerate = newIntValue;
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                console.error("`count` attribute should have an integer value.", e);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.customElements.define("rubiks-cube-scrambler", RubiksCubeScrambler);
 | 
			
		||||
| 
						 | 
				
			
			@ -1,169 +0,0 @@
 | 
			
		|||
// Eryn Wells <eryn@erynwells.me>
 | 
			
		||||
 | 
			
		||||
class RubySwitch extends HTMLElement {
 | 
			
		||||
    static controlSizeInPixels = 32;
 | 
			
		||||
    static thumbTransitionDuration = 0.1;
 | 
			
		||||
 | 
			
		||||
    static settings = [
 | 
			
		||||
        {
 | 
			
		||||
            id: "ruby-switch-none",
 | 
			
		||||
            value: "none",
 | 
			
		||||
            label: "あ"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            id: "ruby-switch-both",
 | 
			
		||||
            value: "both",
 | 
			
		||||
            label: "<ruby>あ<rt>a</rt></ruby>",
 | 
			
		||||
            default: true
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            id: "ruby-switch-hidden",
 | 
			
		||||
            value: "hidden",
 | 
			
		||||
            label: "ab"
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    #root;
 | 
			
		||||
    #thumb;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.#updateValue(RubySwitch.settings.find(obj => obj.default).value);
 | 
			
		||||
        this.addEventListener("RubyStyleChanged", event => {
 | 
			
		||||
            this.#updateValue(event.detail.style);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.#root = this.attachShadow({ mode: "closed" });
 | 
			
		||||
        this.#buildShadowDOM();
 | 
			
		||||
 | 
			
		||||
        this.#updateThumbPosition(this.#root.querySelector(".control[data-default]"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #updateValue(style) {
 | 
			
		||||
        this.setAttribute("value", style);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get #stylesheet() {
 | 
			
		||||
        const controlSize = RubySwitch.controlSizeInPixels;
 | 
			
		||||
        const halfControlSize = controlSize / 2;
 | 
			
		||||
 | 
			
		||||
        return `
 | 
			
		||||
            #ruby-controls {
 | 
			
		||||
                box-sizing: border-box;
 | 
			
		||||
                display: inline-block;
 | 
			
		||||
                position: relative;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            #controls {
 | 
			
		||||
                border: none;
 | 
			
		||||
                box-sizing: border-box;
 | 
			
		||||
                display: inline grid;
 | 
			
		||||
                grid-template-columns: repeat(3, ${controlSize}px);
 | 
			
		||||
                margin: 0;
 | 
			
		||||
                overflow: none;
 | 
			
		||||
                padding: 0;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            #thumb {
 | 
			
		||||
                box-sizing: border-box;
 | 
			
		||||
                box-shadow: 2px 2px 6px #ccc;
 | 
			
		||||
                border: 0.5px solid #aaa;
 | 
			
		||||
                border-radius: ${halfControlSize}px;
 | 
			
		||||
                position: absolute;
 | 
			
		||||
                top: 0;
 | 
			
		||||
                height: ${controlSize}px;
 | 
			
		||||
                width: ${controlSize}px;
 | 
			
		||||
                z-index: 50;
 | 
			
		||||
                transition: left ${RubySwitch.thumbTransitionDuration}s;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .control {
 | 
			
		||||
                aspect-ratio: 1;
 | 
			
		||||
                display: flex;
 | 
			
		||||
                justify-content: center;
 | 
			
		||||
                align-items: center;
 | 
			
		||||
                height: ${controlSize}px;
 | 
			
		||||
                width: ${controlSize}px;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            b {
 | 
			
		||||
                font-weight: normal;
 | 
			
		||||
                cursor: pointer;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            label {
 | 
			
		||||
                z-index: 2;
 | 
			
		||||
                text-align: center;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            #ruby-switch-both {
 | 
			
		||||
            }
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #buildShadowDOM() {
 | 
			
		||||
        const root = this.#root;
 | 
			
		||||
 | 
			
		||||
        const style = document.createElement("style");
 | 
			
		||||
        style.textContent = this.#stylesheet;
 | 
			
		||||
        root.appendChild(style);
 | 
			
		||||
 | 
			
		||||
        let container = document.createElement("div");
 | 
			
		||||
        container.id = "ruby-controls";
 | 
			
		||||
        root.appendChild(container);
 | 
			
		||||
 | 
			
		||||
        let controls = document.createElement("div");
 | 
			
		||||
        controls.id = "controls";
 | 
			
		||||
        container.appendChild(controls);
 | 
			
		||||
 | 
			
		||||
        for (const desc of RubySwitch.settings) {
 | 
			
		||||
            let control = document.createElement("div");
 | 
			
		||||
            control.classList.add("control")
 | 
			
		||||
            control.id = desc.id;
 | 
			
		||||
 | 
			
		||||
            control.dataset.value = desc.value;
 | 
			
		||||
            if (desc.default) {
 | 
			
		||||
                control.dataset.default = "";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            controls.appendChild(control);
 | 
			
		||||
 | 
			
		||||
            control.addEventListener("click", event => {
 | 
			
		||||
                event.stopPropagation();
 | 
			
		||||
                event.preventDefault();
 | 
			
		||||
 | 
			
		||||
                this.#updateThumbPosition(event.currentTarget);
 | 
			
		||||
 | 
			
		||||
                this.#root.dispatchEvent(new CustomEvent("RubyStyleChanged", {
 | 
			
		||||
                    bubbles: true,
 | 
			
		||||
                    composed: true,
 | 
			
		||||
                    detail: {
 | 
			
		||||
                        style: control.dataset.value,
 | 
			
		||||
                    },
 | 
			
		||||
                }));
 | 
			
		||||
            }, { capture: true });
 | 
			
		||||
 | 
			
		||||
            const label = document.createElement("b");
 | 
			
		||||
            label.innerHTML = desc.label;
 | 
			
		||||
            control.appendChild(label);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const thumb = document.createElement("div");
 | 
			
		||||
        this.#thumb = thumb;
 | 
			
		||||
        thumb.id = "thumb";
 | 
			
		||||
        container.appendChild(thumb);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #updateThumbPosition(selectedControl) {
 | 
			
		||||
        const controls = this.#root.querySelector("#controls");
 | 
			
		||||
 | 
			
		||||
        const trackBoundingRect = controls.getBoundingClientRect();
 | 
			
		||||
        const controlBoundingRect = selectedControl.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
        const offset = controlBoundingRect.left - trackBoundingRect.left;
 | 
			
		||||
        this.#thumb.style.left = `${offset}px`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define("ruby-switch", RubySwitch);
 | 
			
		||||
							
								
								
									
										77
									
								
								assets/styles/blog.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,77 @@
 | 
			
		|||
:root {
 | 
			
		||||
    --post-item-highlight-color: #efefef;
 | 
			
		||||
}
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
    :root {
 | 
			
		||||
        --post-item-highlight-color: #121212;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blog > .highlight { margin-block-end: var(--body-item-spacing); }
 | 
			
		||||
 | 
			
		||||
.post-nav {
 | 
			
		||||
    align-items: baseline;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    margin-top: 3.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.post-nav .next {
 | 
			
		||||
    margin-left: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.post-single footer {
 | 
			
		||||
    margin-block-start: var(--body-item-spacing);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blog.list > ul {
 | 
			
		||||
    list-style: none;
 | 
			
		||||
}
 | 
			
		||||
.blog.list > ul > li {
 | 
			
		||||
    align-items: baseline;
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    display: grid;
 | 
			
		||||
    gap: 1rem;
 | 
			
		||||
    grid-template-columns: minmax(min-content, 10vh) minmax(min-content, 3vh) auto max-content;
 | 
			
		||||
    margin-block-end: 0.25rem;
 | 
			
		||||
    transition: background-color 0.25s;
 | 
			
		||||
}
 | 
			
		||||
.blog.list > ul > li:hover {
 | 
			
		||||
    background-color: var(--post-item-highlight-color);
 | 
			
		||||
}
 | 
			
		||||
.blog.list > ul > li > a { color: inherit; }
 | 
			
		||||
.blog.list > ul > li > a:hover { text-decoration: none; }
 | 
			
		||||
.blog.list > ul > li > time:nth-child(2) { text-align: end; }
 | 
			
		||||
.blog.list > ul > li > .draft { align-self: center; }
 | 
			
		||||
 | 
			
		||||
@supports (grid-template-columns: subgrid) {
 | 
			
		||||
    .blog.list {
 | 
			
		||||
        display: grid;
 | 
			
		||||
        gap: 1rem;
 | 
			
		||||
        grid-template-columns: minmax(min-content, 10vh) minmax(min-content, 3vh) auto max-content;
 | 
			
		||||
    }
 | 
			
		||||
    .blog.list > ul {
 | 
			
		||||
        display: grid;
 | 
			
		||||
        row-gap: 0.25rem;
 | 
			
		||||
        grid-column: 1 / -1;
 | 
			
		||||
        grid-template-columns: subgrid;
 | 
			
		||||
        list-style: none;
 | 
			
		||||
    }
 | 
			
		||||
    .blog.list > ul > li {
 | 
			
		||||
        display: grid;
 | 
			
		||||
        grid-column: 1 / -1;
 | 
			
		||||
        grid-template-columns: subgrid;
 | 
			
		||||
        margin-block-end: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .blog.list > h5 {
 | 
			
		||||
        grid-column: 1 / -1;
 | 
			
		||||
        margin-block: 1rem 0;
 | 
			
		||||
    }
 | 
			
		||||
    .blog.list > h5:first-child {
 | 
			
		||||
        margin-block-start: 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blog.list > ul > li > :first-child {
 | 
			
		||||
    text-align: end;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										67
									
								
								assets/styles/development.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,67 @@
 | 
			
		|||
/* development.css
 | 
			
		||||
 * Some styles for development UI.
 | 
			
		||||
 * Eryn Wells <eryn@erynwells.me>
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
details:has(.photo-params.debug) {
 | 
			
		||||
    margin-block-end: var(--body-item-spacing);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.draft {
 | 
			
		||||
    color: red;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    font-family: 'SF Pro', sans-serif;
 | 
			
		||||
    font-size: 1.5rem;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    letter-spacing: 3px;
 | 
			
		||||
    text-transform: uppercase;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.draft:before {
 | 
			
		||||
    content: "[";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.draft:after {
 | 
			
		||||
    content: "]";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#debug-page-info {
 | 
			
		||||
    background-color: var(--background-color);
 | 
			
		||||
    border: 1px solid var(--separator-color);
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    box-shadow: 4px 5px 5px var(--box-shadow-color);
 | 
			
		||||
    font-size: 1.75rem;
 | 
			
		||||
    padding: 0.5rem;
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    left: 2rem;
 | 
			
		||||
    bottom: 2rem;
 | 
			
		||||
    width: max-content;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#debug-page-info > details {
 | 
			
		||||
    margin: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#debug-page-info > summary {
 | 
			
		||||
    font-family: var(--font-family-heading);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#debug-page-info > details > table {
 | 
			
		||||
    border-collapse: collapse;
 | 
			
		||||
    display: block;
 | 
			
		||||
    margin-block-start: 0.5em;
 | 
			
		||||
    margin-inline-start: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#debug-page-info > details > table > tbody > tr > td {
 | 
			
		||||
    border: 1px solid rgb(var(--dk-gray));
 | 
			
		||||
    padding: 0.3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#debug-page-info > details > table > tbody > tr:nth-child(even) {
 | 
			
		||||
    background-color: var(--separator-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo-params.debug {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										166
									
								
								assets/styles/home.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,166 @@
 | 
			
		|||
:root {
 | 
			
		||||
    --animation-offset: 6px;
 | 
			
		||||
    --font-size-max: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    font-size: 2rem;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    margin: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1 {
 | 
			
		||||
    font-size: 5rem;
 | 
			
		||||
    line-height: 1.3;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
}
 | 
			
		||||
@media (max-width: 599px) {
 | 
			
		||||
    h1 { font-size: 4.25rem; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 599px) {
 | 
			
		||||
    html {
 | 
			
		||||
        --font-size-scale-factor: 1.33vmax;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main {
 | 
			
		||||
    margin-block-start: 10vh;
 | 
			
		||||
    width: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main .grid {
 | 
			
		||||
    align-items: baseline;
 | 
			
		||||
    display: grid;
 | 
			
		||||
    gap: 0 2rem;
 | 
			
		||||
    grid-template-columns: minmax(min-content, 1fr) minmax(min-content, 2fr);
 | 
			
		||||
    grid-template-rows: repeat(2, max-content);
 | 
			
		||||
    grid-template-areas:
 | 
			
		||||
        "title blurb"
 | 
			
		||||
        "title nav";
 | 
			
		||||
    padding-inline: 1em;
 | 
			
		||||
    width: min-content;
 | 
			
		||||
}
 | 
			
		||||
@media (max-width: 599px) {
 | 
			
		||||
    main .grid {
 | 
			
		||||
        gap: 1rem 0;
 | 
			
		||||
        grid-template-columns: 1fr;
 | 
			
		||||
        grid-template-rows: repeat(3, max-content);
 | 
			
		||||
        grid-template-areas:
 | 
			
		||||
            "title"
 | 
			
		||||
            "blurb"
 | 
			
		||||
            "nav";
 | 
			
		||||
        justify-items: center;
 | 
			
		||||
        text-align: center;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main h1 {
 | 
			
		||||
    letter-spacing: 0.025em;
 | 
			
		||||
    text-align: end;
 | 
			
		||||
}
 | 
			
		||||
@media (max-width: 599px) {
 | 
			
		||||
    main h1 { text-align: center; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
nav {
 | 
			
		||||
    align-items: baseline;
 | 
			
		||||
    align-self: center;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    list-style: none;
 | 
			
		||||
    text-transform: lowercase;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
p {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1 { grid-area: title; }
 | 
			
		||||
#content { grid-area: blurb }
 | 
			
		||||
nav { grid-area: nav }
 | 
			
		||||
 | 
			
		||||
h1, #content > p, nav { position: relative; }
 | 
			
		||||
 | 
			
		||||
h1 {
 | 
			
		||||
    animation: left-fade-in var(--transition-duration) ease-in-out;
 | 
			
		||||
}
 | 
			
		||||
@media (max-width: 599px) {
 | 
			
		||||
    h1 {
 | 
			
		||||
        animation: top-fade-in var(--transition-duration) ease-in-out;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
#content > p, nav {
 | 
			
		||||
    animation: right-fade-in var(--transition-duration) ease-in-out;
 | 
			
		||||
}
 | 
			
		||||
@media (max-width: 599px) {
 | 
			
		||||
    #content > p, nav {
 | 
			
		||||
        animation: bottom-fade-in var(--transition-duration) ease-in-out;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes left-fade-in {
 | 
			
		||||
    from {
 | 
			
		||||
        opacity: 0%;
 | 
			
		||||
        left: var(--animation-offset);
 | 
			
		||||
    }
 | 
			
		||||
    33% {
 | 
			
		||||
        opacity: 0%;
 | 
			
		||||
        left: var(--animation-offset);
 | 
			
		||||
    }
 | 
			
		||||
    to {
 | 
			
		||||
        opacity: 100%;
 | 
			
		||||
        left: 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes right-fade-in {
 | 
			
		||||
    from {
 | 
			
		||||
        opacity: 0%;
 | 
			
		||||
        left: calc(-1 * var(--animation-offset));
 | 
			
		||||
    }
 | 
			
		||||
    33% {
 | 
			
		||||
        opacity: 0%;
 | 
			
		||||
        left: calc(-1 * var(--animation-offset));
 | 
			
		||||
    }
 | 
			
		||||
    to {
 | 
			
		||||
        opacity: 100%;
 | 
			
		||||
        left: 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes top-fade-in {
 | 
			
		||||
    from {
 | 
			
		||||
        opacity: 0%;
 | 
			
		||||
        bottom: calc(-1 * var(--animation-offset));
 | 
			
		||||
    }
 | 
			
		||||
    33% {
 | 
			
		||||
        opacity: 0%;
 | 
			
		||||
        bottom: calc(-1 * var(--animation-offset));
 | 
			
		||||
    }
 | 
			
		||||
    to {
 | 
			
		||||
        opacity: 100%;
 | 
			
		||||
        bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes bottom-fade-in {
 | 
			
		||||
    from {
 | 
			
		||||
        opacity: 0%;
 | 
			
		||||
        bottom: var(--animation-offset);
 | 
			
		||||
    }
 | 
			
		||||
    33% {
 | 
			
		||||
        opacity: 0%;
 | 
			
		||||
        bottom: var(--animation-offset);
 | 
			
		||||
    }
 | 
			
		||||
    to {
 | 
			
		||||
        opacity: 100%;
 | 
			
		||||
        bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.platter {
 | 
			
		||||
    padding: 1.5rem 3rem;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										192
									
								
								assets/styles/monokai.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,192 @@
 | 
			
		|||
:root {
 | 
			
		||||
    --highlight-background-color: rgb(var(--super-lt-gray));
 | 
			
		||||
    --highlight-foreground-color: var(--foreground-body-color);
 | 
			
		||||
}
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
    :root {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chroma {
 | 
			
		||||
    line-height: var(--body-line-height);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.bg {
 | 
			
		||||
    color: var(--highlight-foreground-color);
 | 
			
		||||
    background-color: var(--highlight-background-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chroma {
 | 
			
		||||
    color: var(--highlight-foreground-color);
 | 
			
		||||
    background-color: var(--highlight-background-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Other */ .chroma .x {  }
 | 
			
		||||
/* Error */ .chroma .err { color: #960050; background-color: #1e0010 }
 | 
			
		||||
/* CodeLine */ .chroma .cl {  }
 | 
			
		||||
/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
 | 
			
		||||
/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
 | 
			
		||||
/* LineHighlight */ .chroma .hl { background-color: #ffffcc }
 | 
			
		||||
/* LineNumbersTable */ .chroma .lnt { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
 | 
			
		||||
/* LineNumbers */ .chroma .ln { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
 | 
			
		||||
/* Keyword */ .chroma .k { color: #00a8c8 }
 | 
			
		||||
/* KeywordConstant */ .chroma .kc { color: #00a8c8 }
 | 
			
		||||
/* KeywordDeclaration */ .chroma .kd { color: #00a8c8 }
 | 
			
		||||
/* KeywordNamespace */ .chroma .kn { color: #f92672 }
 | 
			
		||||
/* KeywordPseudo */ .chroma .kp { color: #00a8c8 }
 | 
			
		||||
/* KeywordReserved */ .chroma .kr { color: #00a8c8 }
 | 
			
		||||
/* KeywordType */ .chroma .kt { color: #00a8c8 }
 | 
			
		||||
/* Name */ .chroma .n { color: #111111 }
 | 
			
		||||
/* NameAttribute */ .chroma .na { color: #75af00 }
 | 
			
		||||
/* NameBuiltin */ .chroma .nb { color: #111111 }
 | 
			
		||||
/* NameBuiltinPseudo */ .chroma .bp { color: #111111 }
 | 
			
		||||
/* NameClass */ .chroma .nc { color: #75af00 }
 | 
			
		||||
/* NameConstant */ .chroma .no { color: #00a8c8 }
 | 
			
		||||
/* NameDecorator */ .chroma .nd { color: #75af00 }
 | 
			
		||||
/* NameEntity */ .chroma .ni { color: #111111 }
 | 
			
		||||
/* NameException */ .chroma .ne { color: #75af00 }
 | 
			
		||||
/* NameFunction */ .chroma .nf { color: #75af00 }
 | 
			
		||||
/* NameFunctionMagic */ .chroma .fm { color: #111111 }
 | 
			
		||||
/* NameLabel */ .chroma .nl { color: #111111 }
 | 
			
		||||
/* NameNamespace */ .chroma .nn { color: #111111 }
 | 
			
		||||
/* NameOther */ .chroma .nx { color: #75af00 }
 | 
			
		||||
/* NameProperty */ .chroma .py { color: #111111 }
 | 
			
		||||
/* NameTag */ .chroma .nt { color: #f92672 }
 | 
			
		||||
/* NameVariable */ .chroma .nv { color: #111111 }
 | 
			
		||||
/* NameVariableClass */ .chroma .vc { color: #111111 }
 | 
			
		||||
/* NameVariableGlobal */ .chroma .vg { color: #111111 }
 | 
			
		||||
/* NameVariableInstance */ .chroma .vi { color: #111111 }
 | 
			
		||||
/* NameVariableMagic */ .chroma .vm { color: #111111 }
 | 
			
		||||
/* Literal */ .chroma .l { color: #ae81ff }
 | 
			
		||||
/* LiteralDate */ .chroma .ld { color: #d88200 }
 | 
			
		||||
/* LiteralString */ .chroma .s { color: #d88200 }
 | 
			
		||||
/* LiteralStringAffix */ .chroma .sa { color: #d88200 }
 | 
			
		||||
/* LiteralStringBacktick */ .chroma .sb { color: #d88200 }
 | 
			
		||||
/* LiteralStringChar */ .chroma .sc { color: #d88200 }
 | 
			
		||||
/* LiteralStringDelimiter */ .chroma .dl { color: #d88200 }
 | 
			
		||||
/* LiteralStringDoc */ .chroma .sd { color: #d88200 }
 | 
			
		||||
/* LiteralStringDouble */ .chroma .s2 { color: #d88200 }
 | 
			
		||||
/* LiteralStringEscape */ .chroma .se { color: #8045ff }
 | 
			
		||||
/* LiteralStringHeredoc */ .chroma .sh { color: #d88200 }
 | 
			
		||||
/* LiteralStringInterpol */ .chroma .si { color: #d88200 }
 | 
			
		||||
/* LiteralStringOther */ .chroma .sx { color: #d88200 }
 | 
			
		||||
/* LiteralStringRegex */ .chroma .sr { color: #d88200 }
 | 
			
		||||
/* LiteralStringSingle */ .chroma .s1 { color: #d88200 }
 | 
			
		||||
/* LiteralStringSymbol */ .chroma .ss { color: #d88200 }
 | 
			
		||||
/* LiteralNumber */ .chroma .m { color: #ae81ff }
 | 
			
		||||
/* LiteralNumberBin */ .chroma .mb { color: #ae81ff }
 | 
			
		||||
/* LiteralNumberFloat */ .chroma .mf { color: #ae81ff }
 | 
			
		||||
/* LiteralNumberHex */ .chroma .mh { color: #ae81ff }
 | 
			
		||||
/* LiteralNumberInteger */ .chroma .mi { color: #ae81ff }
 | 
			
		||||
/* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff }
 | 
			
		||||
/* LiteralNumberOct */ .chroma .mo { color: #ae81ff }
 | 
			
		||||
/* Operator */ .chroma .o { color: #f92672 }
 | 
			
		||||
/* OperatorWord */ .chroma .ow { color: #f92672 }
 | 
			
		||||
/* Punctuation */ .chroma .p { color: #111111 }
 | 
			
		||||
/* Comment */ .chroma .c { color: #75715e }
 | 
			
		||||
/* CommentHashbang */ .chroma .ch { color: #75715e }
 | 
			
		||||
/* CommentMultiline */ .chroma .cm { color: #75715e }
 | 
			
		||||
/* CommentSingle */ .chroma .c1 { color: #75715e }
 | 
			
		||||
/* CommentSpecial */ .chroma .cs { color: #75715e }
 | 
			
		||||
/* CommentPreproc */ .chroma .cp { color: #75715e }
 | 
			
		||||
/* CommentPreprocFile */ .chroma .cpf { color: #75715e }
 | 
			
		||||
/* Generic */ .chroma .g {  }
 | 
			
		||||
/* GenericDeleted */ .chroma .gd {  }
 | 
			
		||||
/* GenericEmph */ .chroma .ge { font-style: italic }
 | 
			
		||||
/* GenericError */ .chroma .gr {  }
 | 
			
		||||
/* GenericHeading */ .chroma .gh {  }
 | 
			
		||||
/* GenericInserted */ .chroma .gi {  }
 | 
			
		||||
/* GenericOutput */ .chroma .go {  }
 | 
			
		||||
/* GenericPrompt */ .chroma .gp {  }
 | 
			
		||||
/* GenericStrong */ .chroma .gs { font-weight: bold }
 | 
			
		||||
/* GenericSubheading */ .chroma .gu {  }
 | 
			
		||||
/* GenericTraceback */ .chroma .gt {  }
 | 
			
		||||
/* GenericUnderline */ .chroma .gl {  }
 | 
			
		||||
/* TextWhitespace */ .chroma .w {  }
 | 
			
		||||
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
    /* Background */ .bg { color: #f8f8f2; background-color: #272822 }
 | 
			
		||||
    /* PreWrapper */ .chroma { color: #f8f8f2; background-color: #272822; }
 | 
			
		||||
    /* Other */ .chroma .x {  }
 | 
			
		||||
    /* Error */ .chroma .err { color: #960050; background-color: #1e0010 }
 | 
			
		||||
    /* CodeLine */ .chroma .cl {  }
 | 
			
		||||
    /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
 | 
			
		||||
    /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
 | 
			
		||||
    /* LineHighlight */ .chroma .hl { background-color: #ffffcc }
 | 
			
		||||
    /* LineNumbersTable */ .chroma .lnt { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
 | 
			
		||||
    /* LineNumbers */ .chroma .ln { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
 | 
			
		||||
    /* Keyword */ .chroma .k { color: #66d9ef }
 | 
			
		||||
    /* KeywordConstant */ .chroma .kc { color: #66d9ef }
 | 
			
		||||
    /* KeywordDeclaration */ .chroma .kd { color: #66d9ef }
 | 
			
		||||
    /* KeywordNamespace */ .chroma .kn { color: #f92672 }
 | 
			
		||||
    /* KeywordPseudo */ .chroma .kp { color: #66d9ef }
 | 
			
		||||
    /* KeywordReserved */ .chroma .kr { color: #66d9ef }
 | 
			
		||||
    /* KeywordType */ .chroma .kt { color: #66d9ef }
 | 
			
		||||
    /* Name */ .chroma .n {  }
 | 
			
		||||
    /* NameAttribute */ .chroma .na { color: #a6e22e }
 | 
			
		||||
    /* NameBuiltin */ .chroma .nb { color: inherit; }
 | 
			
		||||
    /* NameBuiltinPseudo */ .chroma .bp {  }
 | 
			
		||||
    /* NameClass */ .chroma .nc { color: #a6e22e }
 | 
			
		||||
    /* NameConstant */ .chroma .no { color: #66d9ef }
 | 
			
		||||
    /* NameDecorator */ .chroma .nd { color: #a6e22e }
 | 
			
		||||
    /* NameEntity */ .chroma .ni {  }
 | 
			
		||||
    /* NameException */ .chroma .ne { color: #a6e22e }
 | 
			
		||||
    /* NameFunction */ .chroma .nf { color: #a6e22e }
 | 
			
		||||
    /* NameFunctionMagic */ .chroma .fm {  }
 | 
			
		||||
    /* NameLabel */ .chroma .nl {  }
 | 
			
		||||
    /* NameNamespace */ .chroma .nn {  }
 | 
			
		||||
    /* NameOther */ .chroma .nx { color: #a6e22e }
 | 
			
		||||
    /* NameProperty */ .chroma .py {  }
 | 
			
		||||
    /* NameTag */ .chroma .nt { color: #f92672 }
 | 
			
		||||
    /* NameVariable */ .chroma .nv { color: inherit; }
 | 
			
		||||
    /* NameVariableClass */ .chroma .vc {  }
 | 
			
		||||
    /* NameVariableGlobal */ .chroma .vg {  }
 | 
			
		||||
    /* NameVariableInstance */ .chroma .vi {  }
 | 
			
		||||
    /* NameVariableMagic */ .chroma .vm {  }
 | 
			
		||||
    /* Literal */ .chroma .l { color: #ae81ff }
 | 
			
		||||
    /* LiteralDate */ .chroma .ld { color: #e6db74 }
 | 
			
		||||
    /* LiteralString */ .chroma .s { color: #e6db74 }
 | 
			
		||||
    /* LiteralStringAffix */ .chroma .sa { color: #e6db74 }
 | 
			
		||||
    /* LiteralStringBacktick */ .chroma .sb { color: #e6db74 }
 | 
			
		||||
    /* LiteralStringChar */ .chroma .sc { color: #e6db74 }
 | 
			
		||||
    /* LiteralStringDelimiter */ .chroma .dl { color: #e6db74 }
 | 
			
		||||
    /* LiteralStringDoc */ .chroma .sd { color: #e6db74 }
 | 
			
		||||
    /* LiteralStringDouble */ .chroma .s2 { color: #e6db74 }
 | 
			
		||||
    /* LiteralStringEscape */ .chroma .se { color: #ae81ff }
 | 
			
		||||
    /* LiteralStringHeredoc */ .chroma .sh { color: #e6db74 }
 | 
			
		||||
    /* LiteralStringInterpol */ .chroma .si { color: #e6db74 }
 | 
			
		||||
    /* LiteralStringOther */ .chroma .sx { color: #e6db74 }
 | 
			
		||||
    /* LiteralStringRegex */ .chroma .sr { color: #e6db74 }
 | 
			
		||||
    /* LiteralStringSingle */ .chroma .s1 { color: #e6db74 }
 | 
			
		||||
    /* LiteralStringSymbol */ .chroma .ss { color: #e6db74 }
 | 
			
		||||
    /* LiteralNumber */ .chroma .m { color: #ae81ff }
 | 
			
		||||
    /* LiteralNumberBin */ .chroma .mb { color: #ae81ff }
 | 
			
		||||
    /* LiteralNumberFloat */ .chroma .mf { color: #ae81ff }
 | 
			
		||||
    /* LiteralNumberHex */ .chroma .mh { color: #ae81ff }
 | 
			
		||||
    /* LiteralNumberInteger */ .chroma .mi { color: #ae81ff }
 | 
			
		||||
    /* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff }
 | 
			
		||||
    /* LiteralNumberOct */ .chroma .mo { color: #ae81ff }
 | 
			
		||||
    /* Operator */ .chroma .o { color: #f92672 }
 | 
			
		||||
    /* OperatorWord */ .chroma .ow { color: #f92672 }
 | 
			
		||||
    /* Punctuation */ .chroma .p { color: inherit; }
 | 
			
		||||
    /* Comment */ .chroma .c { color: #75715e }
 | 
			
		||||
    /* CommentHashbang */ .chroma .ch { color: #75715e }
 | 
			
		||||
    /* CommentMultiline */ .chroma .cm { color: #75715e }
 | 
			
		||||
    /* CommentSingle */ .chroma .c1 { color: #75715e }
 | 
			
		||||
    /* CommentSpecial */ .chroma .cs { color: #75715e }
 | 
			
		||||
    /* CommentPreproc */ .chroma .cp { color: #75715e }
 | 
			
		||||
    /* CommentPreprocFile */ .chroma .cpf { color: #75715e }
 | 
			
		||||
    /* Generic */ .chroma .g {  }
 | 
			
		||||
    /* GenericDeleted */ .chroma .gd { color: #f92672 }
 | 
			
		||||
    /* GenericEmph */ .chroma .ge { font-style: italic }
 | 
			
		||||
    /* GenericError */ .chroma .gr {  }
 | 
			
		||||
    /* GenericHeading */ .chroma .gh {  }
 | 
			
		||||
    /* GenericInserted */ .chroma .gi { color: #a6e22e }
 | 
			
		||||
    /* GenericOutput */ .chroma .go {  }
 | 
			
		||||
    /* GenericPrompt */ .chroma .gp {  }
 | 
			
		||||
    /* GenericStrong */ .chroma .gs { font-weight: bold }
 | 
			
		||||
    /* GenericSubheading */ .chroma .gu { color: #75715e }
 | 
			
		||||
    /* GenericTraceback */ .chroma .gt {  }
 | 
			
		||||
    /* GenericUnderline */ .chroma .gl {  }
 | 
			
		||||
    /* TextWhitespace */ .chroma .w {  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										167
									
								
								assets/styles/photos.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,167 @@
 | 
			
		|||
:root {
 | 
			
		||||
    --date-item-background-color: rgb(var(--lt-gray));
 | 
			
		||||
 | 
			
		||||
    --photo-params-background-color: rgb(var(--lt-gray));
 | 
			
		||||
    --photo-params-container-background-color: rgb(var(--super-lt-gray));
 | 
			
		||||
    --photo-params-color: rgb(var(--sub-dk-gray));
 | 
			
		||||
    --photo-params-border-color: rgb(var(--super-lt-gray));
 | 
			
		||||
}
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
    :root {
 | 
			
		||||
        --date-item-background-color: rgb(var(--dk-gray));
 | 
			
		||||
 | 
			
		||||
        --photo-params-background-color: rgb(var(--dk-gray));
 | 
			
		||||
        --photo-params-container-background-color: rgb(var(--sub-dk-gray));
 | 
			
		||||
        --photo-params-color: rgb(var(--super-lt-gray));
 | 
			
		||||
        --photo-params-border-color: rgb(var(--sub-dk-gray));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photos.list {
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    max-width: none;
 | 
			
		||||
    padding: 0 var(--body-item-spacing);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photos.list {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
 | 
			
		||||
    gap: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photos.page > nav {
 | 
			
		||||
    margin-block-end: var(--body-item-spacing);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photos.list > a {
 | 
			
		||||
    display: block;
 | 
			
		||||
    line-height: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photos.list > a > img {
 | 
			
		||||
    border-radius: 3px;
 | 
			
		||||
    image-orientation: from-image;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photos.list > div {
 | 
			
		||||
    background-color: var(--date-item-background-color);
 | 
			
		||||
    border-radius: 3px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photos.list > div > h6 {
 | 
			
		||||
    display: block;
 | 
			
		||||
    font-size: 5rem;
 | 
			
		||||
    margin: 0 auto;
 | 
			
		||||
    letter-spacing: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photos.list > div > h6 > span {
 | 
			
		||||
    text-align: end;
 | 
			
		||||
    width: min-content;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    padding-inline-end: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photos.list > div > h6 > span::after {
 | 
			
		||||
    top: 6px;
 | 
			
		||||
    right: 0px;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    display: block;
 | 
			
		||||
    content: "⏵︎";
 | 
			
		||||
    font-size: 80%;
 | 
			
		||||
}
 | 
			
		||||
@media (max-width: calc(24px + 400px + 4px)) {
 | 
			
		||||
    .photos.list > div > h6 > span {
 | 
			
		||||
        width: max-content;
 | 
			
		||||
        padding-block: calc(0.25 * var(--body-item-spacing));
 | 
			
		||||
    }
 | 
			
		||||
    .photos.list > div > h6 > span::after {
 | 
			
		||||
        content: "";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo-params {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo-params > .container {
 | 
			
		||||
    display: block;
 | 
			
		||||
    background-color: var(--photo-params-container-background-color);
 | 
			
		||||
    border-radius: 10px;
 | 
			
		||||
    margin: var(--body-item-spacing) auto;
 | 
			
		||||
    padding: calc(var(--body-item-spacing) / 2);
 | 
			
		||||
    width: 66%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo-params > .container > table {
 | 
			
		||||
    background-color: var(--photo-params-background-color);
 | 
			
		||||
    color: var(--photo-params-color);
 | 
			
		||||
    border-collapse: collapse;
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    table-layout: fixed;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo-params table > thead > tr > td {
 | 
			
		||||
    border-bottom: 1px solid var(--photo-params-border-color);
 | 
			
		||||
    font-size: 80%;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo-params tr.exposure-attributes > td {
 | 
			
		||||
    border-top: 1px solid var(--photo-params-border-color);
 | 
			
		||||
    border-left: 1px solid var(--photo-params-border-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo-params tr.exposure-attributes > td:first-child {
 | 
			
		||||
    border-left: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo-params td {
 | 
			
		||||
    font-size: 75%;
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo-params td:last-child {
 | 
			
		||||
    text-align: end;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo-params td:first-child {
 | 
			
		||||
    text-align: start;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo-params .make-model {
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo-params .size {
 | 
			
		||||
    border-left: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo-params .location {
 | 
			
		||||
    border-right: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo-params.debug thead {
 | 
			
		||||
    font-size: 2rem;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    border-bottom: 2px solid rgb(var(--dk-gray));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo-params.debug {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo-params.debug td {
 | 
			
		||||
    border: 1px solid var(--photo-params-border-color);
 | 
			
		||||
    overflow: scroll;
 | 
			
		||||
    vertical-align: top;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										64
									
								
								assets/styles/railroad.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,64 @@
 | 
			
		|||
:root {
 | 
			
		||||
    --rect-fill: rgba(var(--mid-blue), 0.1);
 | 
			
		||||
}
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
    :root {
 | 
			
		||||
        --rect-fill: rgb(var(--mid-blue), 0.9);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
svg.railroad-diagram path {
 | 
			
		||||
    stroke-width: 2;
 | 
			
		||||
    stroke: var(--html-color);
 | 
			
		||||
    fill: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
svg.railroad-diagram text {
 | 
			
		||||
    font-family: var(--font-family-monospace);
 | 
			
		||||
    text-anchor: middle;
 | 
			
		||||
    white-space: pre;
 | 
			
		||||
    fill: var(--html-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
svg.railroad-diagram text.diagram-text {
 | 
			
		||||
    font-size: 11px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
svg.railroad-diagram text.diagram-arrow {
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
svg.railroad-diagram text.label {
 | 
			
		||||
    text-anchor: start;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
svg.railroad-diagram text.comment {
 | 
			
		||||
    font: italic 12px var(--font-family-monospace);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
svg.railroad-diagram g.non-terminal text {
 | 
			
		||||
    /*font-style: italic;*/
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
svg.railroad-diagram rect {
 | 
			
		||||
    stroke-width: 2;
 | 
			
		||||
    stroke: var(--html-color);
 | 
			
		||||
    fill: var(--rect-fill);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
svg.railroad-diagram rect.group-box {
 | 
			
		||||
    stroke: gray;
 | 
			
		||||
    stroke-dasharray: 10 5;
 | 
			
		||||
    fill: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
svg.railroad-diagram path.diagram-text {
 | 
			
		||||
    stroke-width: 2;
 | 
			
		||||
    stroke: var(--html-color);
 | 
			
		||||
    fill: var(--html-background-color);
 | 
			
		||||
    cursor: help;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
svg.railroad-diagram g.diagram-text:hover path.diagram-text {
 | 
			
		||||
    fill: var(--rect-fill);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										627
									
								
								assets/styles/root.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,627 @@
 | 
			
		|||
@layer reset, root, template, page;
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
    --black: 0, 0, 0;
 | 
			
		||||
    --sub-dk-gray: 16, 16, 16;
 | 
			
		||||
    --dk-gray: 32, 32, 32;
 | 
			
		||||
    --super-dk-gray: 80, 80, 80;
 | 
			
		||||
    --mid-gray: 128, 128, 128;
 | 
			
		||||
    --sub-lt-gray: 175, 175, 175;
 | 
			
		||||
    --lt-gray: 223, 223, 223;
 | 
			
		||||
    --super-lt-gray: 240, 240, 240;
 | 
			
		||||
    --white: 255, 255, 255;
 | 
			
		||||
 | 
			
		||||
    --lt-blue: 69, 212, 243;
 | 
			
		||||
    --mid-blue: 26, 169, 239;
 | 
			
		||||
    --dk-blue: 63, 46, 231;
 | 
			
		||||
    --purple: 161, 49, 232;
 | 
			
		||||
    --lilac: 187, 121, 245;
 | 
			
		||||
 | 
			
		||||
    --site-nav-link-color: rgb(var(--mid-blue));
 | 
			
		||||
 | 
			
		||||
    --separator-color: rgb(var(--lt-gray));
 | 
			
		||||
    --header-border-color: var(--separator-color);
 | 
			
		||||
    --footer-border-color: var(--separator-color);
 | 
			
		||||
 | 
			
		||||
    --box-shadow-color: rgba(var(--lt-gray), 0.8);
 | 
			
		||||
    --header-box-shadow-color: var(--box-shadow-color);
 | 
			
		||||
 | 
			
		||||
    --font-family-body: Verdana, Helvetica, sans-serif;
 | 
			
		||||
    --font-family-monospace: "Courier New", Courier, monospace;
 | 
			
		||||
    --font-family-heading: Museo_Slab, Tahoma, sans-serif;
 | 
			
		||||
    --font-family-site-heading: Museo_Slab, Tahoma, sans-serif;
 | 
			
		||||
    --font-size-min: 6px;
 | 
			
		||||
    --font-size-max: 8px;
 | 
			
		||||
 | 
			
		||||
    --body-font-size: 2rem;
 | 
			
		||||
    --body-item-spacing: 1em;
 | 
			
		||||
    --body-line-height: 1.5;
 | 
			
		||||
    --body-header-line-height: 1.1;
 | 
			
		||||
    --body-code-background-color: rgb(var(--super-lt-gray));
 | 
			
		||||
 | 
			
		||||
    --heading-color: rgb(var(--black));
 | 
			
		||||
    --header-series-arrow-foreground-color: rgb(var(--sub-dk-gray));
 | 
			
		||||
 | 
			
		||||
    --html-background-color: rgb(var(--white));
 | 
			
		||||
    --html-color: rgba(var(--black), 0.8);
 | 
			
		||||
 | 
			
		||||
    --nav-bulleted-spacing: 0.75rem;
 | 
			
		||||
 | 
			
		||||
    --platter-background-color: rgba(var(--white), var(--platter-background-opacity));
 | 
			
		||||
    --platter-background-opacity: 0.6;
 | 
			
		||||
    --platter-backdrop-filter: blur(10px);
 | 
			
		||||
 | 
			
		||||
    --content-width: 80rem;
 | 
			
		||||
 | 
			
		||||
    --tag-foreground-color: rgb(var(--super-dk-gray));
 | 
			
		||||
    --tag-background-color: rgb(var(--super-lt-gray));
 | 
			
		||||
    --tag-spacer-foreground-color: rgb(var(--super-dk-gray));
 | 
			
		||||
    --tag-hover-background-color: rgb(var(--sub-lt-gray));
 | 
			
		||||
 | 
			
		||||
    --transition-duration: 0.7s;
 | 
			
		||||
 | 
			
		||||
    --social-menu-padding: 1rem;
 | 
			
		||||
    --menu-icon-size: 20px;
 | 
			
		||||
    --twitter-icon: url(/icons/twitter.svg);
 | 
			
		||||
    --github-icon: url(/icons/github.svg);
 | 
			
		||||
    --instagram-icon: url(/icons/instagram.svg);
 | 
			
		||||
    --feed-icon: url(/icons/rss.svg);
 | 
			
		||||
}
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
    :root {
 | 
			
		||||
        --separator-color: rgb(var(--dk-gray));
 | 
			
		||||
        --box-shadow-color: rgba(var(--dk-gray), 0.8);
 | 
			
		||||
        --body-code-background-color: rgb(var(--dk-gray));
 | 
			
		||||
 | 
			
		||||
        --heading-color: rgb(var(--white));
 | 
			
		||||
        --header-series-arrow-foreground-color: rgb(var(--super-dk-gray));
 | 
			
		||||
 | 
			
		||||
        --html-background-color: rgb(var(--black));
 | 
			
		||||
        --html-color: rgba(var(--white), 0.8);
 | 
			
		||||
 | 
			
		||||
        --platter-background-color: rgba(var(--black), var(--platter-background-opacity));
 | 
			
		||||
        --platter-backdrop-filter: brightness(0.66) blur(10px);
 | 
			
		||||
 | 
			
		||||
        --tag-foreground-color: rgb(var(--sub-lt-gray));
 | 
			
		||||
        --tag-background-color: rgb(var(--dk-gray));
 | 
			
		||||
        --tag-spacer-foreground-color: rgb(var(--super-dk-gray));
 | 
			
		||||
        --tag-hover-background-color: rgb(var(--super-dk-gray));
 | 
			
		||||
        --tag-hover-foreground-color: rgb(var(--mid-gray));
 | 
			
		||||
 | 
			
		||||
        --twitter-icon: url(/icons/twitter-dark.svg);
 | 
			
		||||
        --github-icon: url(/icons/github-dark.svg);
 | 
			
		||||
        --instagram-icon: url(/icons/instagram-dark.svg);
 | 
			
		||||
        --feed-icon: url(/icons/rss-dark.svg);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@font-face {
 | 
			
		||||
    font-family: "Museo_Slab";
 | 
			
		||||
    src: url("{{ `/fonts/Museo_Slab_500.woff2` | relURL }}") format("woff2"),
 | 
			
		||||
        url("{{ `/fonts/Museo_Slab_500.woff` | relURL }}") format("woff");
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
    font-style: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@layer reset {
 | 
			
		||||
    body, button, h1, h2, h3, h4, h5, h6, input, ol, ul, p, pre, textarea {
 | 
			
		||||
        padding: 0; margin: 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a {
 | 
			
		||||
    color: rgb(var(--mid-blue));
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
a:hover {
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
    text-underline-offset: 0.12em;
 | 
			
		||||
}
 | 
			
		||||
a:visited { color: none; }
 | 
			
		||||
 | 
			
		||||
blockquote {
 | 
			
		||||
    border-inline-start: 4px solid var(--separator-color);
 | 
			
		||||
    padding-inline-start: 2rem;
 | 
			
		||||
    margin-inline-start: 2rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
    font-size: var(--body-font-size);
 | 
			
		||||
    line-height: var(--body-line-height);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
code {
 | 
			
		||||
    background-color: var(--body-code-background-color);
 | 
			
		||||
    border-radius: 0.25rem;
 | 
			
		||||
    font-family: var(--font-family-monospace);
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    padding-inline: 0.6rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
figcaption {
 | 
			
		||||
    font-size: 75%;
 | 
			
		||||
    line-height: var(--body-line-height);
 | 
			
		||||
    margin-block-start: 0.2em;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
figure {
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    line-height: 1;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    margin-block: 0 var(--body-item-spacing);
 | 
			
		||||
    margin-inline: 0;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    max-width: var(--content-width);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
figure.bordered {
 | 
			
		||||
    padding: 0.5rem;
 | 
			
		||||
    border: 2px solid #eee;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
figure a,
 | 
			
		||||
figure a:hover {
 | 
			
		||||
    border: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
figure img {
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    height: auto;
 | 
			
		||||
    max-width: var(--content-width);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
figure svg {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
figure .youtube {
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    line-height: 1;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    max-width: var(--content-width);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer.site {
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    font-size: 1.6rem;
 | 
			
		||||
    margin-block: var(--body-item-spacing);
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer.site > ul {
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    list-style: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer.site > .slogans {
 | 
			
		||||
    margin-block-end: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer.site > .slogans > li {
 | 
			
		||||
    margin-block-end: 0;
 | 
			
		||||
    margin-inline-start: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer.site > p {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer.site p + p {
 | 
			
		||||
    margin-top: 0.25rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 740px) {
 | 
			
		||||
    footer.site .slogans span {
 | 
			
		||||
        white-space: nowrap;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1 { font-size: 1.6em; }
 | 
			
		||||
h2 { font-size: 1.5em; }
 | 
			
		||||
h3 { font-size: 1.4em; }
 | 
			
		||||
h4 { font-size: 1.3em; }
 | 
			
		||||
h5 { font-size: 1.3em; }
 | 
			
		||||
h6 { font-size: var(--body-font-size); }
 | 
			
		||||
h5, h6 { font-family: var(--font-family-body); }
 | 
			
		||||
 | 
			
		||||
h1, h2, h3, h4, h5, h6 {
 | 
			
		||||
    color: var(--heading-color);
 | 
			
		||||
    font-family: var(--font-family-heading);
 | 
			
		||||
    font-weight: 600;
 | 
			
		||||
    margin-block: 3.5rem 1rem;
 | 
			
		||||
    letter-spacing: 0.08em;
 | 
			
		||||
    line-height: var(--body-header-line-height);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1.site {
 | 
			
		||||
    color: rgb(var(--mid-blue));
 | 
			
		||||
    font-size: 2.5em;
 | 
			
		||||
}
 | 
			
		||||
@supports (background-clip: text) {
 | 
			
		||||
    h1.site {
 | 
			
		||||
        background:
 | 
			
		||||
            radial-gradient(circle at 20% 70%, rgb(var(--purple)), transparent 40%),
 | 
			
		||||
            radial-gradient(circle at 30% 30%, rgb(var(--lt-blue)), rgb(var(--mid-blue)) 20%, transparent 80%),
 | 
			
		||||
            radial-gradient(ellipse at 95% 20%, rgb(var(--dk-blue)), rgb(var(--mid-blue)) 70%, transparent 80%),
 | 
			
		||||
            radial-gradient(circle at 100% 100%, rgb(var(--purple)), rgb(var(--lilac)) 100%),
 | 
			
		||||
            radial-gradient(circle at 45% 100%, rgb(var(--lilac)), rgb(var(--purple)) 60%),
 | 
			
		||||
            radial-gradient(ellipse at 50% 50%, rgb(var(--dk-blue)), transparent 80%);
 | 
			
		||||
        -webkit-background-clip: text;
 | 
			
		||||
        background-clip: text;
 | 
			
		||||
        color: transparent;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1.site > a { color: inherit; }
 | 
			
		||||
h1.site > a:hover { text-decoration: none; }
 | 
			
		||||
 | 
			
		||||
header.site {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    z-index: 10000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
header.site > .grid > h1 {
 | 
			
		||||
    font-family: var(--font-family-site-heading);
 | 
			
		||||
    font-size: 2em;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    order: 1;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
header.site > .grid {
 | 
			
		||||
    align-items: baseline;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    gap: 0 0.5em;
 | 
			
		||||
    margin: 0 auto;
 | 
			
		||||
    max-width: var(--content-width);
 | 
			
		||||
    padding: 1.5rem 3rem;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
header.site > .grid > nav:first-of-type {
 | 
			
		||||
    justify-content: start;
 | 
			
		||||
    order: 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
header.site > .grid > nav:last-of-type {
 | 
			
		||||
    justify-content: end;
 | 
			
		||||
    order: 3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 516px) {
 | 
			
		||||
    header.site > .platter {
 | 
			
		||||
        border-left: none;
 | 
			
		||||
        border-radius: 0;
 | 
			
		||||
        border-right: none;
 | 
			
		||||
        border-top: none;
 | 
			
		||||
    }
 | 
			
		||||
    header.site > .grid {
 | 
			
		||||
        max-width: none;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 435px) {
 | 
			
		||||
    header.site > .grid > nav:first-of-type {
 | 
			
		||||
        order: 5;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html {
 | 
			
		||||
    --font-size-scale-factor: 1vmax;
 | 
			
		||||
 | 
			
		||||
    background-color: var(--html-background-color);
 | 
			
		||||
    color: var(--html-color);
 | 
			
		||||
    font-family: var(--font-family-body);
 | 
			
		||||
    font-size: clamp(var(--font-size-min), var(--font-size-scale-factor), var(--font-size-max));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
img {
 | 
			
		||||
    height: auto;
 | 
			
		||||
    max-width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
img.circular {
 | 
			
		||||
    shape-outside: circle(50%);
 | 
			
		||||
    -webkit-clip-path: circle(50%);
 | 
			
		||||
    clip-path: circle(50%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main {
 | 
			
		||||
    max-width: var(--content-width);
 | 
			
		||||
    margin: var(--body-item-spacing) auto;
 | 
			
		||||
    padding-inline: var(--body-item-spacing);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main > header {
 | 
			
		||||
    margin-bottom: var(--body-item-spacing);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
header.site > .grid > nav {
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    font-size: max(1.5rem, 80%);
 | 
			
		||||
    justify-content: start;
 | 
			
		||||
    letter-spacing: 0.12em;
 | 
			
		||||
    list-style: none;
 | 
			
		||||
    text-transform: lowercase;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
header.site > .grid > nav > li {
 | 
			
		||||
    color: var(--site-nav-link-color);
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
header.site > .grid > nav > .active { font-weight: bold; }
 | 
			
		||||
 | 
			
		||||
main > :first-child { margin-block-start: 0; }
 | 
			
		||||
main > :last-child { margin-block-end: 0; }
 | 
			
		||||
 | 
			
		||||
nav.bulleted > li:first-child::before {
 | 
			
		||||
    color: var(--html-color);
 | 
			
		||||
    content: "";
 | 
			
		||||
    margin-inline: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
nav.bulleted > li::before {
 | 
			
		||||
    color: var(--heading-color);
 | 
			
		||||
    content: "•";
 | 
			
		||||
    font-size: 60%;
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
    opacity: 80%;
 | 
			
		||||
    margin-inline: var(--nav-bulleted-spacing);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
nav.social > li { margin-inline-start: var(--nav-bulleted-spacing); }
 | 
			
		||||
nav.social > li:first-child { margin-inline-start: 0; }
 | 
			
		||||
 | 
			
		||||
p {
 | 
			
		||||
    letter-spacing: 0.025em;
 | 
			
		||||
    line-height: var(--body-line-height);
 | 
			
		||||
    margin-block-end: var(--body-item-spacing);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ul, ol, dl {
 | 
			
		||||
    margin-block-end: var(--body-item-spacing);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ul,
 | 
			
		||||
ol,
 | 
			
		||||
dl,
 | 
			
		||||
ul > li,
 | 
			
		||||
ol > li,
 | 
			
		||||
dl > dt,
 | 
			
		||||
ul > li > ul,
 | 
			
		||||
ul > li > ol,
 | 
			
		||||
ol > li > ul,
 | 
			
		||||
ol > li > ol {
 | 
			
		||||
    margin-inline-start: calc(var(--body-item-spacing) + 2px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
header > span.series {
 | 
			
		||||
    font-size: 1.75rem;
 | 
			
		||||
    letter-spacing: 1px;
 | 
			
		||||
    margin-inline-start: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
header > span.series::before {
 | 
			
		||||
    color: var(--header-series-arrow-foreground-color);
 | 
			
		||||
    content: "↳";
 | 
			
		||||
    margin-inline-end: 0.25em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.post-list {
 | 
			
		||||
    list-style: none;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
}
 | 
			
		||||
@supports (display: subgrid) {
 | 
			
		||||
    .post-list {
 | 
			
		||||
        align-items: baseline;
 | 
			
		||||
        display: grid;
 | 
			
		||||
        gap: 1em;
 | 
			
		||||
        grid-template-columns:
 | 
			
		||||
            minmax(150px, max-content) minmax(20px, max-content) 3px auto;
 | 
			
		||||
        grid-template-areas: "month day line title";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.post-list li {
 | 
			
		||||
    align-items: baseline;
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    display: grid;
 | 
			
		||||
    gap: 1em;
 | 
			
		||||
    padding: 0.1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.post-title {
 | 
			
		||||
    align-items: baseline;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    gap: 0 4rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.post-title h1 {
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    font-size: 1.8em;
 | 
			
		||||
    margin-block: 0;
 | 
			
		||||
    padding-bottom: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.post-title > .post-date {
 | 
			
		||||
    color: var(--light-dim);
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    font-size: 1.75rem;
 | 
			
		||||
    inline-size: min-content;
 | 
			
		||||
    letter-spacing: 1px;
 | 
			
		||||
    margin-block-start: 0.5rem;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.p5-sketch {
 | 
			
		||||
    display: block;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.social {
 | 
			
		||||
    display: block;
 | 
			
		||||
    letter-spacing: 2px;
 | 
			
		||||
    margin-left: auto;
 | 
			
		||||
    order: 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.social > li > a {
 | 
			
		||||
    background-image: var(--url);
 | 
			
		||||
    background-repeat: no-repeat;
 | 
			
		||||
    background-size: var(--menu-icon-size);
 | 
			
		||||
    display: block;
 | 
			
		||||
    height: var(--menu-icon-size);
 | 
			
		||||
    width: var(--menu-icon-size);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.social > li > a > span {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.social > li + li:before {
 | 
			
		||||
    color: var(--dark);
 | 
			
		||||
    padding: 0 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer > .tags {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    margin-inline-start: 0;
 | 
			
		||||
    padding-inline-start: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer > .tags > li {
 | 
			
		||||
    background-color: var(--tag-background-color);
 | 
			
		||||
    color: var(--tag-foreground-color);
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    font-size: 75%;
 | 
			
		||||
    letter-spacing: 1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer > .tags > li:first-child {
 | 
			
		||||
    margin-inline-start: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer > .tags > li:hover {
 | 
			
		||||
    background-color: var(--tag-hover-background-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer > .tags > li > a {
 | 
			
		||||
    color: inherit;
 | 
			
		||||
    display: block;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    padding: 0.6rem 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer > .tags > li > a:hover {
 | 
			
		||||
    color: var(--tag-hover-foreground-color);
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer > .tags > li + li { margin-inline-start: 1rem; }
 | 
			
		||||
footer > .tags > li.chevron + li { margin-inline-start: 0 }
 | 
			
		||||
 | 
			
		||||
footer > .tags > .chevron {
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    border: 0;
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
    color: var(--tag-spacer-foreground-color);
 | 
			
		||||
    background-color: inherit;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    margin-inline-start: 0;
 | 
			
		||||
    padding-inline-start: 1px;
 | 
			
		||||
    width: 2rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer > .tags > .chevron:hover {
 | 
			
		||||
    color: var(--tag-spacer-foreground-color);
 | 
			
		||||
    background-color: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table {
 | 
			
		||||
    border: 1px solid var(--separator-color);
 | 
			
		||||
    border-collapse: collapse;
 | 
			
		||||
    margin-block-end: var(--body-item-spacing);
 | 
			
		||||
    margin-inline: auto;
 | 
			
		||||
    width: 50%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
td, th {
 | 
			
		||||
    border: 1px solid var(--separator-color);
 | 
			
		||||
    padding-inline: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.youtube iframe {
 | 
			
		||||
    aspect-ratio: 16 / 9;
 | 
			
		||||
    margin-bottom: -3px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** SYNTAX HIGHLIGHTING **/
 | 
			
		||||
 | 
			
		||||
.highlight code {
 | 
			
		||||
    background-color: inherit;
 | 
			
		||||
    margin-block: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.highlight .line {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-columns: max-content auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.highlight .ln a {
 | 
			
		||||
    -webkit-user-select: none;
 | 
			
		||||
    -moz-use-select: none;
 | 
			
		||||
    -ms-user-select: none;
 | 
			
		||||
    user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.highlight .cl {
 | 
			
		||||
    white-space: pre-wrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** HELPER CLASSES **/
 | 
			
		||||
 | 
			
		||||
.centered { text-align: center; }
 | 
			
		||||
.float-right { float: right; }
 | 
			
		||||
 | 
			
		||||
.nobreak { white-space: nowrap; }
 | 
			
		||||
.noselect {
 | 
			
		||||
    cursor: default;
 | 
			
		||||
    -webkit-user-select: none;
 | 
			
		||||
    -moz-use-select: none;
 | 
			
		||||
    -ms-user-select: none;
 | 
			
		||||
    user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.platter {
 | 
			
		||||
    -webkit-backdrop-filter: var(--platter-backdrop-filter);
 | 
			
		||||
    backdrop-filter: var(--platter-backdrop-filter);
 | 
			
		||||
    background-color: var(--platter-background-color);
 | 
			
		||||
    border: 1px solid var(--header-border-color);
 | 
			
		||||
    border-radius: 12px;
 | 
			
		||||
    box-shadow: 3px 4px 4px var(--header-box-shadow-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.visible { visibility: visible; }
 | 
			
		||||
							
								
								
									
										2
									
								
								config/_default/author.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
name: Eryn Wells
 | 
			
		||||
email: eryn@erynwells.me
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
baseURL: https://erynwells.me/
 | 
			
		||||
languageCode: en-US
 | 
			
		||||
title: ~eryn
 | 
			
		||||
copyright: Copyright © 2020—2024 Eryn Wells
 | 
			
		||||
title: Erynwells.me
 | 
			
		||||
copyright: Copyright © 2020—2022 Eryn Wells
 | 
			
		||||
defaultContentLanguage: en
 | 
			
		||||
enableEmoji: true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,4 @@
 | 
			
		|||
en:
 | 
			
		||||
  languageName: English
 | 
			
		||||
  weight: 1
 | 
			
		||||
es:
 | 
			
		||||
  languageName: Español
 | 
			
		||||
  weight: 2
 | 
			
		||||
jp:
 | 
			
		||||
  languageName: 日本語
 | 
			
		||||
  weight: 3
 | 
			
		||||
tok:
 | 
			
		||||
  languageName: toki pona
 | 
			
		||||
  weight: 4
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,5 @@
 | 
			
		|||
goldmark:
 | 
			
		||||
  renderer:
 | 
			
		||||
    unsafe: true
 | 
			
		||||
  parser:
 | 
			
		||||
    attribute:
 | 
			
		||||
      block: true
 | 
			
		||||
      title: true
 | 
			
		||||
highlight:
 | 
			
		||||
  anchorLineNos: true
 | 
			
		||||
  lineNos: false
 | 
			
		||||
  lineNos: true
 | 
			
		||||
  lineNumbersInTable: false
 | 
			
		||||
  noClasses: false
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,19 +11,13 @@ main:
 | 
			
		|||
    name: About
 | 
			
		||||
    url: /about/
 | 
			
		||||
    weight: 30
 | 
			
		||||
  - identifier: feed
 | 
			
		||||
    name: feed
 | 
			
		||||
    url: /feed.atom
 | 
			
		||||
    weight: 40
 | 
			
		||||
    params:
 | 
			
		||||
      style: file
 | 
			
		||||
social:
 | 
			
		||||
  - identifier: mastodon
 | 
			
		||||
    name: Mastodon
 | 
			
		||||
    url: https://mastodon.social/@erynofwales
 | 
			
		||||
  - identifier: twitter
 | 
			
		||||
    name: Twitter
 | 
			
		||||
    url: https://twitter.com/erynofwales
 | 
			
		||||
    weight: 10
 | 
			
		||||
    params:
 | 
			
		||||
      shortName: mst
 | 
			
		||||
      shortName: tw
 | 
			
		||||
  - identifier: github
 | 
			
		||||
    name: Github
 | 
			
		||||
    url: https://github.com/erynofwales
 | 
			
		||||
| 
						 | 
				
			
			@ -37,7 +31,7 @@ social:
 | 
			
		|||
    params:
 | 
			
		||||
      shortName: ig
 | 
			
		||||
  - identifier: feed
 | 
			
		||||
    name: feed
 | 
			
		||||
    name: Feed
 | 
			
		||||
    url: /feed.atom
 | 
			
		||||
    weight: 40
 | 
			
		||||
    params:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +0,0 @@
 | 
			
		|||
hugoVersion:
 | 
			
		||||
  extended: false
 | 
			
		||||
  min: "0.116.0"
 | 
			
		||||
replacements: >-
 | 
			
		||||
  github.com/erynofwales/hugo-theme-feeds/v2 -> feeds,
 | 
			
		||||
  github.com/erynofwales/hugo-theme-termlite/v2 -> termlite,
 | 
			
		||||
  github.com/erynofwales/hugo-theme-photostream/v2 -> photostream,
 | 
			
		||||
  github.com/erynofwales/hugo-resource-builders/v2 -> resource-builders,
 | 
			
		||||
  github.com/erynofwales/hugo-image-utilities/v2 -> image-utils
 | 
			
		||||
imports:
 | 
			
		||||
  - path: github.com/erynofwales/hugo-theme-termlite/v2
 | 
			
		||||
  - path: github.com/erynofwales/hugo-theme-feeds/v2
 | 
			
		||||
  - path: github.com/erynofwales/hugo-theme-photostream/v2
 | 
			
		||||
  - path: github.com/erynofwales/hugo-resource-builders/v2
 | 
			
		||||
  - path: github.com/erynofwales/hugo-image-utilities/v2
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,3 @@
 | 
			
		|||
home: [HTML, Atom]
 | 
			
		||||
section: [HTML, Atom]
 | 
			
		||||
taxonomy: [HTML]
 | 
			
		||||
term: [HTML]
 | 
			
		||||
home: [HTML, Atom, RSS]
 | 
			
		||||
page: [HTML, JSON]
 | 
			
		||||
section: [HTML, Atom, RSS]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,20 +1,4 @@
 | 
			
		|||
author:
 | 
			
		||||
  name: Eryn Wells
 | 
			
		||||
  email: eryn@erynwells.me
 | 
			
		||||
 | 
			
		||||
shortTitle: Eryn Wells
 | 
			
		||||
 | 
			
		||||
twitter: erynofwales
 | 
			
		||||
github: erynofwales
 | 
			
		||||
instagram: erynofwales
 | 
			
		||||
description: Home page of Eryn Rachel Wells
 | 
			
		||||
 | 
			
		||||
blog:
 | 
			
		||||
  yearLimit: 3
 | 
			
		||||
 | 
			
		||||
photostream:
 | 
			
		||||
  yearLimit: 3
 | 
			
		||||
 | 
			
		||||
photos:
 | 
			
		||||
  gridSize: 200
 | 
			
		||||
  thumbnailSize: 600
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,2 @@
 | 
			
		|||
blog: blog/:year/:month/:slug/
 | 
			
		||||
photos: photos/:year/:month/:slug/
 | 
			
		||||
twitter: twitter/:year/:month/:slug/
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,2 +1,2 @@
 | 
			
		|||
x:
 | 
			
		||||
twitter:
 | 
			
		||||
  enableDNT: true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,2 +1,2 @@
 | 
			
		|||
x:
 | 
			
		||||
twitter:
 | 
			
		||||
  disableInlineCSS: true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,20 +1,7 @@
 | 
			
		|||
---
 | 
			
		||||
layout: single
 | 
			
		||||
draft: true
 | 
			
		||||
params:
 | 
			
		||||
  renderHeadingAnchors: false
 | 
			
		||||
title: Eryn Rachel Wells
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
¡Hola! Me llamo Eryn Wells. Este es mi sitio web. ¡Bienvenidos!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Personal
 | 
			
		||||
 | 
			
		||||
Soy mujer queer, 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Profesional
 | 
			
		||||
 | 
			
		||||
Desde 2011, he trabajado como ingeniera de software en varios companías alrededor del área
 | 
			
		||||
del Bahía de San Francisco. En 2016, me uní a [Apple][]. Hoy, trabajo en
 | 
			
		||||
tecnologías que 
 | 
			
		||||
{{< nobreak >}}Ingeniera de software,{{< /nobreak >}}
 | 
			
		||||
alfarera, música, y
 | 
			
		||||
{{< nobreak >}}nerd en general.{{< /nobreak >}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,66 +1,5 @@
 | 
			
		|||
---
 | 
			
		||||
layout: single
 | 
			
		||||
params:
 | 
			
		||||
  renderHeadingAnchors: false
 | 
			
		||||
title: Eryn Rachel Wells
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
Hi, I'm Eryn Wells. This is my website. Welcome!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Latest
 | 
			
		||||
 | 
			
		||||
Here are some of my most recent posts.
 | 
			
		||||
 | 
			
		||||
{{< home/latest >}}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Personal
 | 
			
		||||
 | 
			
		||||
I'm a queer woman, {{< tess >}}' partner, and mom of [two cats][cats]. I
 | 
			
		||||
was born in Seattle, {{< abbr Washington >}}WA{{< /abbr >}} and grew up in
 | 
			
		||||
Phoenix, {{< abbr Arizona >}}AZ{{< /abbr >}}. I attended [Oberlin College][]
 | 
			
		||||
where I got a degree in Computer Science. My pronouns are [she/her][pronouns]. 
 | 
			
		||||
 | 
			
		||||
You can read more about me on my [about][ab] page, or [get in touch][where-am-i].
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Professional
 | 
			
		||||
 | 
			
		||||
I've worked as a software engineer since 2011 for a variety of companies around
 | 
			
		||||
the San Francisco Bay Area. I joined [Apple][] in 2016, where I currently work
 | 
			
		||||
on password management and authentication technologies.
 | 
			
		||||
 | 
			
		||||
My [résumé][r] has all the details.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Hobbies
 | 
			
		||||
 | 
			
		||||
When I'm not working, you can reliably find me hacking on this website or [some
 | 
			
		||||
other coding project][src]. I'm also a musician: I play piano, Irish tin
 | 
			
		||||
whistle, and modular synthesizer. Occasionally I [record things][sc] or offer
 | 
			
		||||
them [for sale][sc]. I love outer space and astronomy; I will always get excited
 | 
			
		||||
to look at the moon with you, or check out anything through a telescope. I enjoy
 | 
			
		||||
[photograhy][p], mostly as a travel hobby. And I've been practicing
 | 
			
		||||
[iaidō][mjer], a traditional Japanese sword art, at [Nishi Kaigan Iaidō
 | 
			
		||||
Dōjō][nkid] since early 2024. Other things I've been into include: bread baking,
 | 
			
		||||
bicycling, calligraphy, ceramics, and knitting. 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
[Apple]: https://apple.com
 | 
			
		||||
[ab]: {{< ref "/about" >}}
 | 
			
		||||
[b]: {{< ref "/blog" >}}
 | 
			
		||||
[bc]: https://erynwells.bandcamp.com/releases
 | 
			
		||||
[cats]: {{< ref "/cats" >}}
 | 
			
		||||
[eml]: mailto:Eryn%20Wells<eryn@erynwells.me>
 | 
			
		||||
[src]: https://source.erynwells.me/eryn
 | 
			
		||||
[ig]: https://www.instagram.com/erynofwales
 | 
			
		||||
[m]: https://mastodon.social/@erynofwales
 | 
			
		||||
[n]: {{< ref "/now" >}}
 | 
			
		||||
[Oberlin College]: https://www.oberlin.edu
 | 
			
		||||
[p]: {{< ref "/photos" >}}
 | 
			
		||||
[pronouns]: http://pronoun.is/she
 | 
			
		||||
[r]: {{< ref "/resume" >}}
 | 
			
		||||
[sc]: https://soundcloud.com/purlsnbeeps
 | 
			
		||||
[where-am-i]: {{< ref "/about/where-am-i" >}}
 | 
			
		||||
[mjer]: https://en.wikipedia.org/wiki/Mus%C5%8D_Jikiden_Eishin-ry%C5%AB
 | 
			
		||||
[nkid]: https://iaido.org
 | 
			
		||||
Software engineer, potter, musician, and overall nerd.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,16 +1,14 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Hi! 👋🏻"
 | 
			
		||||
layout: single
 | 
			
		||||
date: 2022-09-03T12:14:32-07:00
 | 
			
		||||
draft: false
 | 
			
		||||
resources:
 | 
			
		||||
  - name: me
 | 
			
		||||
    src: me.jpeg
 | 
			
		||||
    params:
 | 
			
		||||
      alt: >
 | 
			
		||||
        Me, wearing a hat and smiling slightly, standing in front of a stone
 | 
			
		||||
        background.
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
{{% section class=content--small-right-column %}}
 | 
			
		||||
{{< circular_image id=me name=me class="float-right" width=200
 | 
			
		||||
  alt="A photo of me, wearing a hat, standing in front of a stone background">}}
 | 
			
		||||
 | 
			
		||||
I'm Eryn. My pronouns are [she/her][p]. I'm a queer trans woman. I live in San
 | 
			
		||||
Francisco, CA, on the unceded ancestral lands of the Ramaytush Ohlone people,
 | 
			
		||||
| 
						 | 
				
			
			@ -20,10 +18,6 @@ I attended [Oberlin College][ob] where I got a degree in Computer Science.
 | 
			
		|||
 | 
			
		||||
I speak English natively, and Spanish too, though I always need more practice.
 | 
			
		||||
 | 
			
		||||
{{< circular_image id=me name=me class="content--right-column" width=200 >}}
 | 
			
		||||
 | 
			
		||||
{{% /section %}}
 | 
			
		||||
 | 
			
		||||
## Hobbies
 | 
			
		||||
 | 
			
		||||
I've been a musician for most of my life. I started on the piano at age seven,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +1,4 @@
 | 
			
		|||
@layer page {
 | 
			
		||||
  main > section > p:not(:last-child) {
 | 
			
		||||
    margin-bottom: var(--body-item-spacing);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  p:has(img#me) {
 | 
			
		||||
    display: inline;
 | 
			
		||||
    grid-column: unset;
 | 
			
		||||
    margin-bottom: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  img#me {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    shape-outside: circle(55%);
 | 
			
		||||
img#me {
 | 
			
		||||
    width: min(200px, 25%);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,16 +3,11 @@ title: "Where to Find Me"
 | 
			
		|||
date: 2022-11-11T08:35:26-08:00
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
Here's a list of places you can find me online. You can often find me on
 | 
			
		||||
services not listed here with the `erynofwales` or `erynrwells` handles.
 | 
			
		||||
Here's a list of places you can find me online.
 | 
			
		||||
 | 
			
		||||
## Social Media
 | 
			
		||||
 | 
			
		||||
I'm really only on Instagram and Mastodon these days. My Twitter account is
 | 
			
		||||
still live, as an archive, but I don't post on it or look at it. Ditto for
 | 
			
		||||
Facebook.
 | 
			
		||||
 | 
			
		||||
- Facebook: [erynofwales](https://www.facebook.com/erynofwales)
 | 
			
		||||
- Cohost: [@eryn](https://cohost.org/eryn)
 | 
			
		||||
- Instagram: [@erynofwales](https://instagram.com/erynofwales)
 | 
			
		||||
- Mastodon: [@erynofwales](https://mastodon.social/@erynofwales)
 | 
			
		||||
- Twitter: [@erynofwales](https://twitter.com/erynofwales)
 | 
			
		||||
| 
						 | 
				
			
			@ -20,10 +15,9 @@ Facebook.
 | 
			
		|||
## Content
 | 
			
		||||
 | 
			
		||||
- Bandcamp: [erynwells](https://erynwells.bandcamp.com/releases)
 | 
			
		||||
- GitHub: [erynofwales](https://github.com/erynofwales)
 | 
			
		||||
- Soundcloud: [purlsnbeeps](https://soundcloud.com/purlsnbeeps)
 | 
			
		||||
- StoryGraph: [erynrwells](https://app.thestorygraph.com/profile/erynrwells)
 | 
			
		||||
- YouTube: [Eryn Wells](https://www.youtube.com/channel/UCWb2pTDlC27R1PucyUPrypA)
 | 
			
		||||
- GitHub: [erynofwales](https://github.com/erynofwales)
 | 
			
		||||
 | 
			
		||||
## The Old Fashioned Way
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Booting a Raspberry Pi Over TFTP"
 | 
			
		||||
date: 2020-10-13T08:31:52-07:00
 | 
			
		||||
description: A writeup of how I set up a Raspberry Pi to boot over TFTP to facilitate an operating system development project.
 | 
			
		||||
draft: false
 | 
			
		||||
series: ["Raspberry Pi OS Development"]
 | 
			
		||||
categories: ["Tech"]
 | 
			
		||||
tags: ["Raspberry Pi", "Networking"]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: 2020
 | 
			
		||||
date: 2020-01-01
 | 
			
		||||
---
 | 
			
		||||
| 
						 | 
				
			
			@ -7,7 +7,7 @@ categories: ["Music"]
 | 
			
		|||
tags: ["Synthesizers", "Electronics", "DIY", "Compositions"]
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
{{< youtube id="gCSwWsxzy_c" title="A timelapse video of me building an Oskitone Scout, set to music produced using the Scout itself" >}}
 | 
			
		||||
{{< figures/youtube id="gCSwWsxzy_c" title="A timelapse video of me building an Oskitone Scout, set to music produced using the Scout itself">}}
 | 
			
		||||
 | 
			
		||||
[Oskitone][oskitone] recently released a new synthesizer: the [Scout][scout].
 | 
			
		||||
It's a small monophonic keyboard synth built around an Arduino. It was a quick
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,4 +41,4 @@ I'm so grateful for every one of these people. We've been friends for years and
 | 
			
		|||
even though our lives have taken us in so many different directions, we've found
 | 
			
		||||
each other again and that is so wonderful.
 | 
			
		||||
 | 
			
		||||
{{< twitter user=erynofwales id=1447951049076056071 >}}
 | 
			
		||||
{{< twitter erynofwales 1447951049076056071 >}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: 2021
 | 
			
		||||
date: 2021-01-01
 | 
			
		||||
---
 | 
			
		||||
| 
						 | 
				
			
			@ -7,7 +7,7 @@ categories: ["Music"]
 | 
			
		|||
tags: ["Eurorack", "Synthesizers", "Recordings", "Performances", "Compositions"]
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
{{< youtube id="sqr7g4P85aM" title="A top-down video of me operating a small Eurorack system made of only three modules. Lights flash, an incorporeal hand turns knobs to sculpt the sound." >}}
 | 
			
		||||
{{< figures/youtube id="sqr7g4P85aM" title="A top-down video of me operating a small Eurorack system made of only three modules. Lights flash, an incorporeal hand turns knobs to sculpt the sound." >}}
 | 
			
		||||
 | 
			
		||||
This is my submission to the [Three Module Challenge][3mc] show put on by
 | 
			
		||||
Colorado Modular Synth Society in late January 2022. This is my first time
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,7 @@ estuve hecho.
 | 
			
		|||
 | 
			
		||||
Es posible que me entusiasme un poco.
 | 
			
		||||
 | 
			
		||||
{{< twitter user=erynofwales id=1510763278691016705 >}}
 | 
			
		||||
{{< twitter erynofwales 1510763278691016705 >}}
 | 
			
		||||
 | 
			
		||||
He mejorado mucho en las últimas semanas. Mis puntajes han crecidos desde 1,000
 | 
			
		||||
hasta el mejor juego hasta ahora en que [obtuve 9401 puntos][over9000]. Quién
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,7 @@ packed with [NetHackWiki][nethackwiki] tabs too, including a pinned one for the
 | 
			
		|||
 | 
			
		||||
I may have gotten a little carried away a time or two.
 | 
			
		||||
 | 
			
		||||
{{< twitter user=erynofwales id=1510763278691016705 >}}
 | 
			
		||||
{{< twitter erynofwales 1510763278691016705 >}}
 | 
			
		||||
 | 
			
		||||
I've gotten much better in that time. My scores have increased from the
 | 
			
		||||
1000-2000 range to my best game so far in which [I scored 9401
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,6 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Roguelikes I Like"
 | 
			
		||||
date: 2022-05-09T08:37:23-07:00
 | 
			
		||||
description: Some roguelikes I’ve enjoyed recently.
 | 
			
		||||
draft: false
 | 
			
		||||
resources:
 | 
			
		||||
  - name: nethack
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,36 +0,0 @@
 | 
			
		|||
<svg class="railroad-diagram" width="408" height="62" viewBox="0 0 408 62">
 | 
			
		||||
<g transform="translate(.5 .5)">
 | 
			
		||||
<g>
 | 
			
		||||
<path d="M20 21v20m10 -20v20m-10 -10h20"></path>
 | 
			
		||||
</g>
 | 
			
		||||
<path d="M40 31h10"></path>
 | 
			
		||||
<g>
 | 
			
		||||
<path d="M50 31h0"></path>
 | 
			
		||||
<path d="M358 31h0"></path>
 | 
			
		||||
<g class="terminal ">
 | 
			
		||||
<path d="M50 31h0"></path>
 | 
			
		||||
<path d="M126 31h0"></path>
 | 
			
		||||
<rect x="50" y="20" width="76" height="22" rx="10" ry="10"></rect>
 | 
			
		||||
<text x="88" y="35"><audio></text>
 | 
			
		||||
</g>
 | 
			
		||||
<path d="M126 31h10"></path>
 | 
			
		||||
<path d="M136 31h10"></path>
 | 
			
		||||
<g class="terminal ">
 | 
			
		||||
<path d="M146 31h0"></path>
 | 
			
		||||
<path d="M230 31h0"></path>
 | 
			
		||||
<rect x="146" y="20" width="84" height="22" rx="10" ry="10"></rect>
 | 
			
		||||
<text x="188" y="35">Analyzer</text>
 | 
			
		||||
</g>
 | 
			
		||||
<path d="M230 31h10"></path>
 | 
			
		||||
<path d="M240 31h10"></path>
 | 
			
		||||
<g class="terminal ">
 | 
			
		||||
<path d="M250 31h0"></path>
 | 
			
		||||
<path d="M358 31h0"></path>
 | 
			
		||||
<rect x="250" y="20" width="108" height="22" rx="10" ry="10"></rect>
 | 
			
		||||
<text x="304" y="35">destination</text>
 | 
			
		||||
</g>
 | 
			
		||||
</g>
 | 
			
		||||
<path d="M358 31h10"></path>
 | 
			
		||||
<path d="M 368 31 h 20 m -10 -10 v 20 m 10 -20 v 20"></path>
 | 
			
		||||
</g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 1 KiB  | 
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Making an Audio Scope with P5.js"
 | 
			
		||||
date: 2022-08-18T20:48:37-07:00
 | 
			
		||||
description: A writeup of a small JavaScript waveform visualizer I made with P5.js.
 | 
			
		||||
draft: false
 | 
			
		||||
categories: ["Tech"]
 | 
			
		||||
tags: ["P5.js", "Programming", "Web", "Art"]
 | 
			
		||||
resources:
 | 
			
		||||
| 
						 | 
				
			
			@ -88,7 +88,22 @@ node to the input of the analyzer node, and the output of the analyzer node to
 | 
			
		|||
the audio context's `destination` node that routes to the computer's speakers.
 | 
			
		||||
Our audio processing graph looks like this:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
{{< figures/railroad id="audioContextDiagram" >}}
 | 
			
		||||
    {{< scripts/railroad >}}
 | 
			
		||||
        return rr.Diagram(
 | 
			
		||||
            rr.Sequence(
 | 
			
		||||
                rr.Terminal("<audio>"),
 | 
			
		||||
                rr.Terminal("Analyzer"),
 | 
			
		||||
                rr.Terminal("destination")));
 | 
			
		||||
    {{< /scripts/railroad >}}
 | 
			
		||||
    {{< scripts/railroad narrow=1 >}}
 | 
			
		||||
    return rr.Diagram(
 | 
			
		||||
        rr.Stack(
 | 
			
		||||
            rr.Terminal("<audio>"),
 | 
			
		||||
            rr.Terminal("Analyzer"),
 | 
			
		||||
            rr.Terminal("destination")));
 | 
			
		||||
    {{< /scripts/railroad >}}
 | 
			
		||||
{{< /figures/railroad >}}
 | 
			
		||||
 | 
			
		||||
By itself the AudioContext doesn't actually play any audio. I'll tackle that
 | 
			
		||||
next.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,6 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Hugo's Dictionary API"
 | 
			
		||||
date: 2022-10-13T10:19:02-07:00
 | 
			
		||||
description: I’ve found Hugo’s API for collections to be difficult to understand. Here’s my attempt to summarize it’s quirks.
 | 
			
		||||
categories: ["Tech"]
 | 
			
		||||
tags: ["Hugo", "Web", "API Design"]
 | 
			
		||||
series: "Erynwells.me Development"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,6 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Lunar Eclipse 🌝"
 | 
			
		||||
date: 2022-11-07T08:37:45-08:00
 | 
			
		||||
description: A quick note about the upcoming lunar eclipse in the morning of 2022-11-08.
 | 
			
		||||
categories: Space
 | 
			
		||||
tags: [Moon, Lunar Eclipse]
 | 
			
		||||
---
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,66 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "My Best Nethack Game (So Far)"
 | 
			
		||||
date: 2022-11-24T09:13:15-05:00
 | 
			
		||||
description: A summary of my best-to-date game of Nethack.
 | 
			
		||||
categories: ["Games"]
 | 
			
		||||
tags: ["Nethack", "Roguelikes", "Video Games"]
 | 
			
		||||
resources:
 | 
			
		||||
  - name: wishing
 | 
			
		||||
    src: wishing.png
 | 
			
		||||
    title:
 | 
			
		||||
  - name: oracle
 | 
			
		||||
    src: oracle.png
 | 
			
		||||
    title:
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
I just finished my best ever game of Nethack. I earned 31,118 points as a level
 | 
			
		||||
9 Valkyrie.
 | 
			
		||||
 | 
			
		||||
Some highlights:
 | 
			
		||||
 | 
			
		||||
I got all the way to the bottom of the Mines, fought several vampires and
 | 
			
		||||
trolls, but somehow completely missed the luckstone. I did pick up a grey stone,
 | 
			
		||||
but it ended up being a touchstone.
 | 
			
		||||
 | 
			
		||||
I got a Wand of Wishing! I was zap testing wands in Minetown and the game asked
 | 
			
		||||
me for a wish! I had no idea what to wish for -- it was my first time getting a
 | 
			
		||||
wish 😱 -- but I had been watching [Adeon's Nethack speedrun][adeon] at the 2017
 | 
			
		||||
Roguelike Celebration and remembered him wishing for some absurdly qualified
 | 
			
		||||
dragon scale mail, so I ended up with a *+2 uncursed silver dragon scale mail*
 | 
			
		||||
that brought my AC down to -10. It saved my butt later on when I ran into a
 | 
			
		||||
bunch of winter wolf pups because it reflected their cold beams.
 | 
			
		||||
 | 
			
		||||
{{< figures/image name=wishing >}}
 | 
			
		||||
 | 
			
		||||
Later on in the mines, I stepped on a polymorph trap that transformed me into an
 | 
			
		||||
ice dragon. That transformation caused my dragon scale mail to fuse into my
 | 
			
		||||
body.  When the transformation ended, I was left with simple dragon scales,
 | 
			
		||||
which were still silver, but no longer had the big defense bonus. Boo. :( I did
 | 
			
		||||
get to lay two eggs that hatched into baby ice dragons. Baby dragons are
 | 
			
		||||
ravenous and indiscrimate.
 | 
			
		||||
 | 
			
		||||
The baby dragons killed the Oracle. Whoops.
 | 
			
		||||
 | 
			
		||||
{{< figures/image name=oracle >}}
 | 
			
		||||
 | 
			
		||||
In the oracle level, I tried to dip my long sword into the fountains around the
 | 
			
		||||
Oracle, hoping to find Excalibur. Just a few turns before, I'd fought a
 | 
			
		||||
gelatinous cube that corroded my sword. I didn't notice at the time though, so
 | 
			
		||||
dipping went horribly wrong... Not only did my corroded long sword become
 | 
			
		||||
*thoroughly rusted*, it also eventually became cursed. Oddly enough, that sword
 | 
			
		||||
was still the best weapon I had, and I used it to the very end. I need to figure
 | 
			
		||||
out how to replace weapons that get degraded like that.
 | 
			
		||||
 | 
			
		||||
I solved Sokoban with some help from the Nethack wiki for the first time. The
 | 
			
		||||
puzzles are hard but I enjoyed thinking through them. The monsters in there,
 | 
			
		||||
especially in the upper levels are *hard*: multiple elementals, several packs of
 | 
			
		||||
winter wolf pups, yetis, apes, and a zruty. I got through the last level and
 | 
			
		||||
discovered a mimic blocking the hallway to the zoo. It killed me.
 | 
			
		||||
 | 
			
		||||
Near the end, I was testing amulets and put on an Amulet of Changing that caused
 | 
			
		||||
me to switch genders. Boo.
 | 
			
		||||
 | 
			
		||||
It's in my [logfile][logfile] now too.
 | 
			
		||||
 | 
			
		||||
[adeon]: https://www.youtube.com/watch?v=rIB0y_kwFuY
 | 
			
		||||
[logfile]: {{< ref "nethack" >}}
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 1 MiB  | 
| 
		 Before Width: | Height: | Size: 1.5 MiB  | 
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: 2022
 | 
			
		||||
date: 2022-01-01
 | 
			
		||||
---
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: 2023
 | 
			
		||||
date: 2023-01-01
 | 
			
		||||
---
 | 
			
		||||
| 
						 | 
				
			
			@ -1,17 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Atom Feed Bug Fixes"
 | 
			
		||||
date: 2023-08-09T08:43:26-07:00
 | 
			
		||||
categories: ["Tech"]
 | 
			
		||||
tags: ["Erynwells.me", "Meta", "Atom"]
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
A kind reader pointed out to me that my Atom feed was incorrect. There were two
 | 
			
		||||
problems. First, I was specifying an incorrect URL in the feed's `<link
 | 
			
		||||
rel="self">` -- it was pointing to a nonexistant feed.xml file. Second, I was
 | 
			
		||||
omitting a `<link>` tag from the entries entirely.
 | 
			
		||||
 | 
			
		||||
Thunderbird didn't like this. With no `<link>` for an entry, it would show the
 | 
			
		||||
feed's `<link>` in it's UI. And that link left users at a 404 page.
 | 
			
		||||
 | 
			
		||||
I pushed a fix this morning. You might have to refresh or resubscribe to pick up
 | 
			
		||||
the changes.
 | 
			
		||||
| 
						 | 
				
			
			@ -1,22 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Chess"
 | 
			
		||||
date: 2023-11-20T14:58:56-08:00
 | 
			
		||||
categories: Chess
 | 
			
		||||
tags: ["Games", "Hobbies"]
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
I've been playing a lot of chess lately. {{< tess >}} and I have been watching
 | 
			
		||||
[Slow Horses][slow-horses] on Apple TV+, and there was a recent episode in which
 | 
			
		||||
a chess game between two characters is a key plot beat. That got me thinking
 | 
			
		||||
about playing again.
 | 
			
		||||
 | 
			
		||||
I learned chess as a kid. My dad taught me. I played in chess clubs in
 | 
			
		||||
elementary and middle school. I was really into it for a while!
 | 
			
		||||
 | 
			
		||||
I have a [Chess.com](chess.com) account now: [erynrwells][chess-com-profile].
 | 
			
		||||
I'm also on [lichess.org](lichess.org): I'm [erynrwells][lichess-profile] there too.
 | 
			
		||||
Send me a friend request or challenge? :)
 | 
			
		||||
 | 
			
		||||
[slow-horses]: https://tv.apple.com/us/show/slow-horses/umc.cmc.2szz3fdt71tl1ulnbp8utgq5o
 | 
			
		||||
[chess-com-profile]: https://www.chess.com/member/erynrwells
 | 
			
		||||
[lichess-profile]: https://lichess.org/@/erynrwells
 | 
			
		||||
| 
						 | 
				
			
			@ -1,13 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Guide to Computing"
 | 
			
		||||
date: 2023-09-23T10:24:35-07:00
 | 
			
		||||
categories: "Tech"
 | 
			
		||||
tags: ["Retro Computing", "Design"]
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
I really enjoyed looking through the images on [Docubyte's Guide to
 | 
			
		||||
Computing][link]. It depicts machines from the early days of modern computing --
 | 
			
		||||
think IBM mainframes, PDP-1's, and lots of midcentury modern design -- in a way
 | 
			
		||||
I found really intriguing.
 | 
			
		||||
 | 
			
		||||
[link]: https://www.docubyte.com/projects/guide-to-computing/
 | 
			
		||||
| 
						 | 
				
			
			@ -1,38 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Hello Chess Friend"
 | 
			
		||||
description: I started building a chess engine in Rust. Here it is.
 | 
			
		||||
date: 2023-12-29T08:29:00-08:00
 | 
			
		||||
series: chess-friend
 | 
			
		||||
categories: Tech
 | 
			
		||||
tags: [Programming, Chess]
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
I started [playing a lot of chess][chess-post] recently. As often happens with
 | 
			
		||||
me, it wasn't very long until I started wondering how I could Do Programming To
 | 
			
		||||
It.
 | 
			
		||||
 | 
			
		||||
I found the mostly excellent, occasionally vague and confusing [Chess
 | 
			
		||||
Programming Wiki][cpwiki] and have been using that as a guide. It helpfully says
 | 
			
		||||
this on it's [Getting Started][cpgs] page:
 | 
			
		||||
 | 
			
		||||
> The **very first step** to writing a chess engine is to write a complete, bug
 | 
			
		||||
> free board representation that knows every rule of chess.
 | 
			
		||||
 | 
			
		||||
As a software engineer, the "bug free" bit cracks me up.
 | 
			
		||||
 | 
			
		||||
My engine is called ChessFriend. It uses [bitboards][cpbb] for its board
 | 
			
		||||
representation. As of this post, I've managed to write a board representation
 | 
			
		||||
that allows me to place pieces of both colors on any square, and I'm hacking
 | 
			
		||||
away at the move generator. I've also written a small command line "board
 | 
			
		||||
explorer" utility that can interact with my board representation. Of course, it
 | 
			
		||||
has a pile of unit tests, helping me inch ever-so-slowly toward that blissful
 | 
			
		||||
bug-free state.
 | 
			
		||||
 | 
			
		||||
It's written in Rust. I've [_mostly_][rust-bc-toot] avoided fighting with the
 | 
			
		||||
borrow checker.
 | 
			
		||||
 | 
			
		||||
[chess-post]: {{< ref "chess" >}}
 | 
			
		||||
[cpwiki]: https://www.chessprogramming.org/Main_Page
 | 
			
		||||
[cpgs]: https://www.chessprogramming.org/Getting_Started
 | 
			
		||||
[cpbb]: https://www.chessprogramming.org/Bitboards
 | 
			
		||||
[rust-bc-toot]: https://mastodon.social/@erynofwales/111637122773195611
 | 
			
		||||
| 
						 | 
				
			
			@ -1,36 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Less Instagram, More Blog"
 | 
			
		||||
description: Resolving, yet again, to blog more and social media less.
 | 
			
		||||
date: 2023-12-27T08:56:44-07:00
 | 
			
		||||
categories: Meta
 | 
			
		||||
tags: [Writing, Resolutions, Habits]
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
I've been thinking the last few days about how to make use of my blog in 2024. I
 | 
			
		||||
made some vague noises in this general direction a few days ago in my [What
 | 
			
		||||
Should I Blog About?][what-to-blog-about] post too.
 | 
			
		||||
 | 
			
		||||
My vision since I started posting more here has been to use it as a place to
 | 
			
		||||
share all sorts of things: stuff I'm working on or thinking about; photos; and
 | 
			
		||||
stories from travel and life.
 | 
			
		||||
 | 
			
		||||
I often fall into a trap when I sit down to write something in which I feel like
 | 
			
		||||
I must first invent the Universe. The need to explain everything from first
 | 
			
		||||
principles seriously hampers my ability (and frankly, desire) to write anything.
 | 
			
		||||
I don't want to only post carefully thought out, highly edited and polished
 | 
			
		||||
pieces, though I certainly hope _some_ of my posts reach that bar. I hope to
 | 
			
		||||
also post quick notes and sketches of ideas. I've enjoyed reading some quicker
 | 
			
		||||
posts from {{< tess >}} and [Elaine][e] this past year, and I'd like to follow
 | 
			
		||||
their example.
 | 
			
		||||
 | 
			
		||||
{{< youtube zSgiXGELjbc >}}
 | 
			
		||||
 | 
			
		||||
I'm not setting myself a specific goal here. The idea is just "more" in a
 | 
			
		||||
certain general direction. I don't want to commit to a specific frequency or
 | 
			
		||||
quality. Instead, I'm hoping this post sets a foundation on which to build a
 | 
			
		||||
sustainable thinking-writing-sharing habit.
 | 
			
		||||
 | 
			
		||||
Thanks for coming along. :)
 | 
			
		||||
 | 
			
		||||
[what-to-blog-about]: {{< ref "what-to-blog-about" >}}
 | 
			
		||||
[e]: https://diplograph.net
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
version https://git-lfs.github.com/spec/v1
 | 
			
		||||
oid sha256:562f6c157c43ef11ae55274b0bd1403c2cbb9a64b82443a2d9f4fb58b32606fd
 | 
			
		||||
size 148873
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "The Long Way to a Small, Angry Planet by Becky Chambers"
 | 
			
		||||
slug: long-way-to-a-small-angry-planet-book
 | 
			
		||||
date: 2023-02-01T09:16:48-08:00
 | 
			
		||||
draft: true
 | 
			
		||||
categories: Books
 | 
			
		||||
tags: ["Science Fiction"]
 | 
			
		||||
series: 2023-books
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1,15 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Mastodon Icon"
 | 
			
		||||
date: 2023-08-11T08:23:25-07:00
 | 
			
		||||
categories: ["Tech"]
 | 
			
		||||
tags: ["Meta", "Erynwells.me", "Web"]
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
I finally got around to replacing the Twitter icon in the site's header with a
 | 
			
		||||
link to my Mastodon page. It was surprisingly tricky because of how I styled and
 | 
			
		||||
layed out those icons. I was able to clean up the SVGs a little bit too.
 | 
			
		||||
 | 
			
		||||
These days I have [way too many social media accounts][where-to-find-me]. I'm
 | 
			
		||||
mostly on Mastodon and Instagram.
 | 
			
		||||
 | 
			
		||||
[where-to-find-me]: {{< ref "/about/where-am-i" >}}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,14 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Nethack Illustrated Guide"
 | 
			
		||||
date: 2023-01-07T08:52:53-08:00
 | 
			
		||||
link: https://thinkmoult.com/nethack-illustrated-guide-mazes-of-menace.html
 | 
			
		||||
categories: Games
 | 
			
		||||
tags: [Nethack, Art, AI]
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
While browsing {{< r nethack >}}, I came across a [post][post] from someone sharing a [collection of AI-generated
 | 
			
		||||
images][guide] that illustrate the story arc of a game of Nethack. Between the images and the prose they added around
 | 
			
		||||
it, I thought they did a fantastic job of capturing the mood of the game.
 | 
			
		||||
 | 
			
		||||
[post]: https://www.reddit.com/r/nethack/comments/zx965y/nethack_an_illustrated_guide_to_the_mazes_of/
 | 
			
		||||
[guide]: https://thinkmoult.com/nethack-illustrated-guide-mazes-of-menace.html
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 77 KiB  | 
| 
		 Before Width: | Height: | Size: 197 KiB  | 
| 
		 Before Width: | Height: | Size: 429 KiB  | 
| 
						 | 
				
			
			@ -1,47 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Netscape Meteors: Retrospective"
 | 
			
		||||
date: 2023-08-05T17:14:40-07:00
 | 
			
		||||
description: Someone shared my Netscape Meteors post on the Orange Website, causing it to be moderately viral for a few days. Here’s an update on the web traffic my server received.
 | 
			
		||||
categories: ["Tech"]
 | 
			
		||||
tags:
 | 
			
		||||
  - History
 | 
			
		||||
  - Meta
 | 
			
		||||
  - Netscape
 | 
			
		||||
  - Web
 | 
			
		||||
  - Web Browsers
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
Last week, I published a small blog post about trying to find the original
 | 
			
		||||
[Netscape "meteors"][meteors] loading animation. At some point that night,
 | 
			
		||||
someone posted it on the orange website, and ... it did some numbers.
 | 
			
		||||
 | 
			
		||||
As of today, five days later, my server has registered just over 200,000 page
 | 
			
		||||
accesses.
 | 
			
		||||
 | 
			
		||||
{{< figures/code >}}
 | 
			
		||||
```txt
 | 
			
		||||
% grep "GET /blog/2023/08/netscape-meteors/" \
 | 
			
		||||
    /var/log/nginx/access.log | wc -l
 | 
			
		||||
  200201
 | 
			
		||||
```
 | 
			
		||||
{{< /figures/code >}}
 | 
			
		||||
 | 
			
		||||
Bandwidth saw a bit of a spike too.
 | 
			
		||||
 | 
			
		||||
{{< figures/image name=bandwidth-2023-08-05.png shouldShowTitle=false >}}
 | 
			
		||||
 | 
			
		||||
To my knowledge this is the first time anything I've published on the internet
 | 
			
		||||
has been picked up by Hacker News. It's jarring to realize so many people have
 | 
			
		||||
visited my website in the last several days, reading this and other things I've
 | 
			
		||||
written, listening to music I've published, and looking through photos I've
 | 
			
		||||
posted. It's a little like having surprise house guests and realizing you
 | 
			
		||||
haven't tidied up in a little while.
 | 
			
		||||
 | 
			
		||||
I only took a brief look at the comments. I was pleased to see they were civil,
 | 
			
		||||
and mostly reminiscing about the days of Netscape and the early web. I had a few
 | 
			
		||||
people reach out to tell me they enjoyed my post too.
 | 
			
		||||
 | 
			
		||||
Thanks, y'all, for reading my little corner of the web, and for your kind
 | 
			
		||||
words.
 | 
			
		||||
 | 
			
		||||
[meteors]: {{< ref "blog/2023/netscape-meteors" >}}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,97 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Netscape Meteors"
 | 
			
		||||
date: 2023-08-01T18:23:33-07:00
 | 
			
		||||
description: I went on a hunt to find the "Meteors" loading animation from Netscape back in the 90s, and wrote up my adventure.
 | 
			
		||||
resources:
 | 
			
		||||
    - name: netscape60
 | 
			
		||||
      title: Netscape Meteor Loading Animation
 | 
			
		||||
      src: netscape-meteors.gif
 | 
			
		||||
    - name: netscape-modified60
 | 
			
		||||
      title: Modified Netscape Meteor Loading Animation, Small
 | 
			
		||||
      src: netscape-meteors-modified-60.gif
 | 
			
		||||
    - name: netscape-modified240
 | 
			
		||||
      title: Modified Netscape Meteor Loading Animation, Large
 | 
			
		||||
      src: netscape-meteors-modified-240.gif
 | 
			
		||||
    - name: rectangular-pixels
 | 
			
		||||
      title: Rectangular Pixels
 | 
			
		||||
      src: rectangular-pixels.png
 | 
			
		||||
      alt: "A zoomed in screenshot of an animation frame with pixel grid enabled,
 | 
			
		||||
            showing rectangular pixels"
 | 
			
		||||
categories: Tech
 | 
			
		||||
tags: ["Netscape", "History", "Web Browsers", "Web"]
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
I went on a small journey the last couple days to find the original Netscape
 | 
			
		||||
Navigator "meteors" animation. This one has a special place in my head and
 | 
			
		||||
heart because it is so clearly connected to my memories of discovering the
 | 
			
		||||
web as a kid. Here it is in its original 60×60 px glory:
 | 
			
		||||
 | 
			
		||||
{{< figures/image name=netscape60 shouldShowTitle=false size=small >}}
 | 
			
		||||
 | 
			
		||||
I started out doing some web searches that turned up several versions. One was
 | 
			
		||||
promising but far too big: 400×400 px. Worse, after some shoddy resize
 | 
			
		||||
attempts, the "pixels" had become rectangular.
 | 
			
		||||
 | 
			
		||||
{{< figures/image name=rectangular-pixels shouldShowTitle=false size=small >}}
 | 
			
		||||
 | 
			
		||||
This would not do.
 | 
			
		||||
 | 
			
		||||
I continued searching, hoping to find the original animations. I found someone's
 | 
			
		||||
[mirror of Netscape 5.0 on Github][gh-netscape]. Then I found some [very old
 | 
			
		||||
versions of Mozilla][moz-netscape] on a Mozilla FTP server. Sadly, the
 | 
			
		||||
animations had been stripped out of these archives. :(
 | 
			
		||||
 | 
			
		||||
Frustrated with hitting several deadends, I complained to {{< tess >}} and
 | 
			
		||||
wondered aloud if anyone might have the original images stashed away somewhere.
 | 
			
		||||
She quipped that if anyone did, it would be Jamie Zawinski.
 | 
			
		||||
 | 
			
		||||
A little later, I posted about it on Mastodon.
 | 
			
		||||
 | 
			
		||||
<iframe
 | 
			
		||||
    src="https://mastodon.social/@erynofwales/110817133916254596/embed"
 | 
			
		||||
    class="mastodon-embed"
 | 
			
		||||
    style="max-width: 100%; border: 0"
 | 
			
		||||
    width="400"
 | 
			
		||||
    allowfullscreen="allowfullscreen">
 | 
			
		||||
</iframe>
 | 
			
		||||
 | 
			
		||||
And wouldn't you know it, a friend tagged [`@jwz`][masto-jwz] asking if he had
 | 
			
		||||
it, and a few moments later I got a reply from [Jamie][jwz] himself.
 | 
			
		||||
 | 
			
		||||
<iframe
 | 
			
		||||
    src="https://mastodon.social/@jwz/110817331045294426/embed"
 | 
			
		||||
    class="mastodon-embed"
 | 
			
		||||
    style="max-width: 100%; border: 0"
 | 
			
		||||
    width="400"
 | 
			
		||||
    allowfullscreen="allowfullscreen">
 | 
			
		||||
</iframe>
 | 
			
		||||
 | 
			
		||||
If you don't know, Jamie Zawinski is well-know for working on several important
 | 
			
		||||
software projects in the '90s. He worked on Netscape Navigator, built and
 | 
			
		||||
maintains [Xscreensaver][xscreensaver], and several other things. Nowadays, he
 | 
			
		||||
owns and runs [DNA Lounge][dna] in San Francisco.
 | 
			
		||||
 | 
			
		||||
There are a lot of neat bits of web browser history on the page he linked --
 | 
			
		||||
totally worth a quick look over -- but most important to the quest at hand, it
 | 
			
		||||
had that Netscape meteors loading animation.
 | 
			
		||||
 | 
			
		||||
The original one has some small artifacts on the left side of frame 10 that
 | 
			
		||||
render as red and orange pixels. These bothered me enough that I made a version
 | 
			
		||||
that replaces those pixels with ones that match the surrounding pixels. Here's
 | 
			
		||||
the modified 60×60 one and a bigger 240×240 px one, for good
 | 
			
		||||
measure:
 | 
			
		||||
 | 
			
		||||
{{< content-grid columns=2 >}}
 | 
			
		||||
  {{< figures/image name="netscape-modified60" shouldShowTitle=false shouldResize=false size=small >}}
 | 
			
		||||
  {{< figures/image name="netscape-modified240" shouldShowTitle=false shouldResize=false size=small >}}
 | 
			
		||||
{{< /content-grid >}}
 | 
			
		||||
 | 
			
		||||
<script src="https://mastodon.social/embed.js" async="async"></script>
 | 
			
		||||
 | 
			
		||||
[gh-netscape]: https://github.com/zii/netscape
 | 
			
		||||
[moz-netscape]: https://ftp.mozilla.org/pub/mozilla/source/
 | 
			
		||||
[masto-jwz]: https://mastodon.social/@jwz
 | 
			
		||||
[jwz]: https://www.jwz.org
 | 
			
		||||
[xscreensaver]: https://www.jwz.org/xscreensaver/
 | 
			
		||||
[dna]: https://www.jwz.org
 | 
			
		||||
[about-jwz]: https://www.jwz.org/doc/about-jwz.html
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 248 KiB  | 
| 
		 Before Width: | Height: | Size: 40 KiB  | 
| 
		 Before Width: | Height: | Size: 36 KiB  | 
| 
		 Before Width: | Height: | Size: 781 KiB  | 
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
version https://git-lfs.github.com/spec/v1
 | 
			
		||||
oid sha256:fa3b6749eb23bff608c4b7233746c6d0577c689786cc69d7354758959e56a9fc
 | 
			
		||||
size 159168
 | 
			
		||||
| 
						 | 
				
			
			@ -1,32 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: Once Upon a Time I Lived on Mars by Kate Greene
 | 
			
		||||
slug: once-upon-a-time-i-lived-on-mars-book
 | 
			
		||||
description: A brief book report.
 | 
			
		||||
date: 2023-02-20T09:16:48-08:00
 | 
			
		||||
date_finished: 2023-02-20T00:00:00-08:00
 | 
			
		||||
categories: Books
 | 
			
		||||
tags: [Memoirs, Space]
 | 
			
		||||
series: 2023-books
 | 
			
		||||
resources:
 | 
			
		||||
    - name: cover
 | 
			
		||||
      src: cover.jpeg
 | 
			
		||||
      alt: "The cover of Once Upon a Time I Lived on Mars by Kate Greene, with a subtitle that reads 'Space, Exploration, and Life on Earth'"
 | 
			
		||||
      title:
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
{{< figures/image name=cover >}}
 | 
			
		||||
 | 
			
		||||
{{< tess >}} got me this book for Christmas 2022 on a whim at a local bookshop. It's a series of essays -- reflections
 | 
			
		||||
and examinations -- of the author's time parcipating in one of NASA's Mars analog missions on Mauna Kea, Hawai'i. She is
 | 
			
		||||
a lesbian and, as luck would have it, lived in the same part of town that Tess and I do! It was fun to read little
 | 
			
		||||
anecdotes about her and her (ex) wife stopping in at shops that we frequent ourselves.
 | 
			
		||||
 | 
			
		||||
I enjoyed reading about her experiences working with NASA in the context of an analog mission. It sounds like they went
 | 
			
		||||
above and beyond to make the mission as close to a real Mars experience as possible, despite being firmly on Earth.
 | 
			
		||||
Communication with mission control was artificially delayed 20 minutes, as it would be on Mars. Going outside required
 | 
			
		||||
putting on bulky spacesuits. And participants were isolated together for six months.
 | 
			
		||||
 | 
			
		||||
She also has several essays in which she reflected on the politics and cost of spaceflight, and what it means for humans
 | 
			
		||||
to explore and exist in space.
 | 
			
		||||
 | 
			
		||||
Support a local bookshop and get it from [Folio Books](https://www.foliosf.com/book/9781250796660). 🙂
 | 
			
		||||
| 
						 | 
				
			
			@ -1,17 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Pajaro Dunes"
 | 
			
		||||
date: 2023-05-30T08:31:34-07:00
 | 
			
		||||
tags: [Travel, Beaches, Tess, EJ, Vacations]
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
{{< tess >}}, EJ, and I took a weekend trip down the coast over Memorial Day
 | 
			
		||||
weekend this year to stay in a beachside condo in Pajaro Dunes, just west of
 | 
			
		||||
Watsonville. We enjoyed hanging out on the beach, playing music and games,
 | 
			
		||||
building [Kiwi Crates][kiwi], and just generally being together. I took a couple
 | 
			
		||||
photos too. :)
 | 
			
		||||
 | 
			
		||||
{{< photo "2023/pajaro-dunes" >}}
 | 
			
		||||
 | 
			
		||||
{{< photo "2023/sunset-over-pajaro-dunes" >}}
 | 
			
		||||
 | 
			
		||||
[kiwi]: https://www.kiwico.com
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
version https://git-lfs.github.com/spec/v1
 | 
			
		||||
oid sha256:ded6e76e0903c82d1db25e8df3f5b68ac72bd07774648ee2dab2c68c0ddbc77f
 | 
			
		||||
size 2616121
 | 
			
		||||
| 
						 | 
				
			
			@ -1,56 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Tahoe Ski Trip"
 | 
			
		||||
date: 2023-01-30T12:40:15-08:00
 | 
			
		||||
draft: true
 | 
			
		||||
categories: Travel
 | 
			
		||||
tags: [Friends, Snowboarding, Snowshoeing]
 | 
			
		||||
resources:
 | 
			
		||||
    - name: cabin
 | 
			
		||||
      src: cabin.jpg
 | 
			
		||||
      title:
 | 
			
		||||
      params:
 | 
			
		||||
        alt: A cozy cabin living room, with wood panel walls, lots of fuzzy, furry pillows, and a high steeply pitched roof. A fireplace is off to the right, and a large couch occupies the middle of the room. The windows fill the wall, floor to ceiling. Outside, you can see many tall pine trees and snow falling.
 | 
			
		||||
    - name: snowshoeing
 | 
			
		||||
      src: snowshoeing.jpg
 | 
			
		||||
      title:
 | 
			
		||||
      params:
 | 
			
		||||
        alt: "A selfie of three people: me and two friends, wearing cold weather gear and standing in the snow. A well-traveled path in the snow meanders through the snow-covered trees."
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
This weekend I took a trip to the north side of Lake Tahoe with a group of coworkers and friends to ski and snowboard,
 | 
			
		||||
and enjoy the mountains and each other's company. We stayed in an AirBnb in Truckee, and spent a couple days up at
 | 
			
		||||
Northstar.
 | 
			
		||||
 | 
			
		||||
{{< figures/image name=cabin >}}
 | 
			
		||||
 | 
			
		||||
We all spent Friday on the mountain. I took an all-day group snowboarding lesson, while the rest did runs all over the
 | 
			
		||||
moutain. My lesson was a small group, just five of us, and it was really great. We were all newbies, and very
 | 
			
		||||
encouraging of each other. Our instructor pushed us quickly through standing and short glides on the board, to longer J
 | 
			
		||||
turns, and traversing the bunny hill. Before lunch, we were doing the Big Easy. I felt like I was starting to get the
 | 
			
		||||
hang of it, and the instructor agreed. After lunch, we went up the Arrow Express lift and took Lumberjack all the way
 | 
			
		||||
down. It was harrowing--I have bad memories of the first drop from the last time I was at Northstart--but we all made
 | 
			
		||||
it! By the end, I was feeling much more confident on the board, though I was also pretty beat up from several rough
 | 
			
		||||
falls throughout the day.
 | 
			
		||||
 | 
			
		||||
We all went to bed pretty sore that night, but the next day we got up and did it all again. There were a _lot_ more
 | 
			
		||||
people on the mountain on Saturday, so wait times for lifts were longer. Despite that I got a bunch of good runs in,
 | 
			
		||||
including one from the top of the moutain with the rest of my group! I ended the day with two runs from the top of the
 | 
			
		||||
moutain, and a sore tail bone, but with much more confidence in my ability to turn and stop on a board. 🤙🏻
 | 
			
		||||
 | 
			
		||||
The third day was a rest day for me. While some went back up the mountain for another day of skiing, a few others of us
 | 
			
		||||
decided to go on a snowshoe hike. This was my first time with snowshoes. They're a bit awkward, but pretty easy to get
 | 
			
		||||
the hang of. We took a loop around a small lake near Donner Lake. I always enjoy the peace that being out in nature
 | 
			
		||||
brings, and it was great to catch up with two of my friends in a smaller group.
 | 
			
		||||
 | 
			
		||||
{{< figures/image name=snowshoeing >}}
 | 
			
		||||
 | 
			
		||||
At the end of the day, we scrambled to amass a stockpile of snowballs for an ambush! The guys in our group had decided
 | 
			
		||||
to go skiing a third day, and were on their way back. When they pulled into the driveway, we attacked! Honestly, it
 | 
			
		||||
wasn't anywhere near a fair fight. 😅 They fought back though, survived the onslaught, and we all had a great time.
 | 
			
		||||
 | 
			
		||||
Spending quality time with friends, going on trips like this, means a lot to me. I've gotten to do trips with friends
 | 
			
		||||
like this a few times over the last couple years, but it's been a bit since I went with _this_ group. I had so much fun
 | 
			
		||||
being up in the moutains, hanging out, playing games, and having long conversations. We're such a good group, and I'm
 | 
			
		||||
grateful to have all of them in my life.
 | 
			
		||||
 | 
			
		||||
Thanks y'all. <3
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
version https://git-lfs.github.com/spec/v1
 | 
			
		||||
oid sha256:00a65714e64f92a4ae67d2d21c8be81341a433f9eabb23c032f835d8cc6a0580
 | 
			
		||||
size 3079324
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
version https://git-lfs.github.com/spec/v1
 | 
			
		||||
oid sha256:6d82f2760711b2c4b7148bf22a141002a937890b8abb8568b4fd3746d8edf0d0
 | 
			
		||||
size 55408
 | 
			
		||||
| 
						 | 
				
			
			@ -1,26 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "The Storyteller by Dave Grohl"
 | 
			
		||||
slug: the-storyteller-book
 | 
			
		||||
date: 2023-01-31T09:17:09-08:00
 | 
			
		||||
date_finished: 2023-01-31T09:17:09-08:00
 | 
			
		||||
series: 2023-books
 | 
			
		||||
categories: Books
 | 
			
		||||
tags: [Memoirs]
 | 
			
		||||
resources:
 | 
			
		||||
    - name: cover
 | 
			
		||||
      src: cover.jpg
 | 
			
		||||
      alt: "The cover of The Storyteller by Dave Grohl: a profile photo of Dave Grohl with the title of the book overlaid."
 | 
			
		||||
      title:
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
{{< figures/image name=cover >}}
 | 
			
		||||
 | 
			
		||||
I checked out [Dave Grohl's new memoir](https://www.davegrohlstoryteller.com) from the San Francisco Public Library as
 | 
			
		||||
an audiobook after a friend recommended it to me. Broadly, it's a series of anecdotes from his life, many of which
 | 
			
		||||
include famous celebrities and musicians.
 | 
			
		||||
 | 
			
		||||
Dave seems like a really genuine person. Throughout the book he expresses his gratitude for the people who've supported
 | 
			
		||||
him along the way. He's been through many challenging experiences too, and reflects on them with a positive attitude. I
 | 
			
		||||
enjoyed his humor and humility, and the pearls of wisdom he'd earned from those experiences.
 | 
			
		||||
 | 
			
		||||
[SFPL](https://sfpl.bibliocommons.com/v2/record/S93C4875170)
 | 
			
		||||
| 
						 | 
				
			
			@ -1,22 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: Trip to Japan
 | 
			
		||||
date: 2023-04-14T21:40:21+09:00
 | 
			
		||||
categories: Travel
 | 
			
		||||
tags:
 | 
			
		||||
  - Travel
 | 
			
		||||
  - japan
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
At the beginning of April, {{< tess >}} and I took a trip to Japan for two
 | 
			
		||||
weeks. She had a work meeting to attend in Tōkyō, and we were lucky to be able
 | 
			
		||||
to extend the trip to take some vacation before her meeting.
 | 
			
		||||
 | 
			
		||||
This was my first trip to Japan. I had been wanting to travel there since I was
 | 
			
		||||
a kid playing Pokémon Red on my OG Game Boy. To say I was excited is a bit of an
 | 
			
		||||
understatement.
 | 
			
		||||
 | 
			
		||||
You can read all about our trip [on my travel log page][series-page]. Tess also
 | 
			
		||||
wrote about it [on her website][tess-post].
 | 
			
		||||
 | 
			
		||||
[series-page]: {{< ref "/series/2023-japan" >}}
 | 
			
		||||
[tess-post]: https://tess.oconnor.cx/2023/04/japan
 | 
			
		||||
| 
						 | 
				
			
			@ -1,13 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: What Should I Blog About?
 | 
			
		||||
date: 2023-12-20T08:06:34-08:00
 | 
			
		||||
link: https://css-irl.info/what-to-blog-about-when-you-dont-know-what-to-blog-about/
 | 
			
		||||
categories: Tech
 | 
			
		||||
tags: [Writing]
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
I came across this handy list of [things to blog about when you don't know what
 | 
			
		||||
to blog about][link]. As someone who often doesn't know what to blog about, it's
 | 
			
		||||
nice to have a list of ideas for what to blog about.
 | 
			
		||||
 | 
			
		||||
[link]: https://css-irl.info/what-to-blog-about-when-you-dont-know-what-to-blog-about/
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
version https://git-lfs.github.com/spec/v1
 | 
			
		||||
oid sha256:7bc98cce772e0c075daed5a6dd9b1c18e6fd19e94ac68197893fd14cb58cf790
 | 
			
		||||
size 83946
 | 
			
		||||
| 
						 | 
				
			
			@ -1,27 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Yerba Buena by Nina LaCour"
 | 
			
		||||
date: 2023-11-23T09:50:06-07:00
 | 
			
		||||
series: 2023-books
 | 
			
		||||
categories: Books
 | 
			
		||||
tags: [Romance, Lesbians, Queerness]
 | 
			
		||||
resources:
 | 
			
		||||
    - name: cover
 | 
			
		||||
      src: cover.jpg
 | 
			
		||||
      title:
 | 
			
		||||
      params:
 | 
			
		||||
        alt: An orange book cover with green leafy sprigs around the edges. Profiles of two women, overlapping and facing opposite directions. "Yerba Buena a novel" is written across the top half.
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
I thought this book was a lesbian romance -- and it is -- but it's so much more
 | 
			
		||||
too. It's the story of two women and the difficult pasts they emerge out of. I
 | 
			
		||||
really enjoyed how LaCour wove together their processing of that trauma with
 | 
			
		||||
growing into young women, making questionable choices, finding themselves, and
 | 
			
		||||
ultimately each other. It's heavy at times, but also beautiful. I really enjoyed
 | 
			
		||||
it. Thank you, {{< tess >}}, for the gift. ❤️
 | 
			
		||||
 | 
			
		||||
Get it at [Folio][folio] in San Francisco, or on [Bookshop.org][bookshop].
 | 
			
		||||
 | 
			
		||||
{{< figures/image name=cover class=content-width >}}
 | 
			
		||||
 | 
			
		||||
[folio]: https://www.foliosf.com/book/9781250810519
 | 
			
		||||
[bookshop]: https://bookshop.org/p/books/yerba-buena-nina-lacour/18721506?ean=9781250810519
 | 
			
		||||
| 
						 | 
				
			
			@ -1,28 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: "You Deserve a Tech Union by Ethan Marcotte"
 | 
			
		||||
date: 2023-12-16T08:19:41-08:00
 | 
			
		||||
draft: true
 | 
			
		||||
series: 2023-books
 | 
			
		||||
categories: Books
 | 
			
		||||
tags: [Unions, Tech]
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
Ethan's book came out in August 2023, and I've been eager to read it since he
 | 
			
		||||
announced it. Unionization has been a hot topic in the US over the last several
 | 
			
		||||
years. I've rooted for workers at Amazon, Google, Starbucks, and Apple to form
 | 
			
		||||
unions and advocate for their rights vis á vis their employers. I think that
 | 
			
		||||
work is so important.
 | 
			
		||||
 | 
			
		||||
This book provides an overview of the history of unions in the US, and in the US
 | 
			
		||||
tech industry, plus some helpful thoughts on how to form unions in your
 | 
			
		||||
workplace.
 | 
			
		||||
 | 
			
		||||
I found his arguments about why unions are important, even in an industrial
 | 
			
		||||
sector considered to be rather plush compared to many others. Many people think
 | 
			
		||||
of unions as organizations that advocate for _more_ privileges and protections
 | 
			
		||||
for workers. However, at least as important as that work, they also ensure that
 | 
			
		||||
workers retain the privileges they already have.
 | 
			
		||||
 | 
			
		||||
Get it from [A Book Apart][aba].
 | 
			
		||||
 | 
			
		||||
[aba]: https://abookapart.com/products/you-deserve-a-tech-union
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: 2024
 | 
			
		||||
date: 2024-01-01
 | 
			
		||||
---
 | 
			
		||||
| 
						 | 
				
			
			@ -1,56 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: ¡Ay Carmela!
 | 
			
		||||
description: |
 | 
			
		||||
  Pero nada pueden bombas<br>
 | 
			
		||||
  Donde sobra corazón
 | 
			
		||||
date: 2024-11-06T08:30:22-08:00
 | 
			
		||||
categories: Politics
 | 
			
		||||
tags:
 | 
			
		||||
  - Music
 | 
			
		||||
  - United States
 | 
			
		||||
  - España
 | 
			
		||||
  - Guerra Civil Española
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
> ```text
 | 
			
		||||
> El ejército del Ebro
 | 
			
		||||
> Rumbala rumbala rum-ba-la
 | 
			
		||||
> El ejército del Ebro
 | 
			
		||||
> Rumbala rumbala rum-ba-la
 | 
			
		||||
>
 | 
			
		||||
> Una noche el río pasó
 | 
			
		||||
> ¡Ay Carmela! ¡Ay Carmela!
 | 
			
		||||
> Una noche el río pasó
 | 
			
		||||
> ¡Ay Carmela! ¡Ay Carmela!
 | 
			
		||||
>
 | 
			
		||||
> Pero nada pueden bombas
 | 
			
		||||
> Rumbala rumbala rum-ba-la
 | 
			
		||||
> Pero nada pueden bombas
 | 
			
		||||
> Rumbala rumbala rum-ba-la
 | 
			
		||||
>
 | 
			
		||||
> Donde sobra corazón
 | 
			
		||||
> ¡Ay Carmela! ¡Ay Carmela!
 | 
			
		||||
> Donde sobra corazón
 | 
			
		||||
> ¡Ay Carmela! ¡Ay Carmela!
 | 
			
		||||
>
 | 
			
		||||
> Contraataques muy rabiosos
 | 
			
		||||
> Rumbala rumbala rum-ba-la
 | 
			
		||||
> Contraataques muy rabiosos
 | 
			
		||||
> Rumbala rumbala rum-ba-la
 | 
			
		||||
>
 | 
			
		||||
> <strong>Deberemos resistir</strong>
 | 
			
		||||
> ¡Ay Carmela! ¡Ay Carmela!
 | 
			
		||||
> Deberemos resistir
 | 
			
		||||
> ¡Ay Carmela! ¡Ay Carmela!
 | 
			
		||||
>
 | 
			
		||||
> Pero igual que combatimos
 | 
			
		||||
> Rumbala rumbala rum-ba-la
 | 
			
		||||
> Pero igual que combatimos
 | 
			
		||||
> Rumbala rumbala rum-ba-la
 | 
			
		||||
>
 | 
			
		||||
> <strong>Prometemos resistir</strong>
 | 
			
		||||
> ¡Ay Carmela! ¡Ay Carmela!
 | 
			
		||||
> Prometemos resistir
 | 
			
		||||
> ¡Ay Carmela! ¡Ay Carmela!
 | 
			
		||||
> ```
 | 
			
		||||
{cite="https://music.apple.com/us/album/ay-carmela/1119265269?i=1119265947" caption="Traditional; emphasis mine"}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,23 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: B612
 | 
			
		||||
date: 2024-03-09T08:38:03-08:00
 | 
			
		||||
description: An open source font I rediscovered recently.
 | 
			
		||||
categories: Tech
 | 
			
		||||
tags: [Fonts]
 | 
			
		||||
resources:
 | 
			
		||||
  - name: specimen
 | 
			
		||||
    src: specimen.png
 | 
			
		||||
    title: B612 y B612 Mono
 | 
			
		||||
    params:
 | 
			
		||||
      alt: "
 | 
			
		||||
      Una muestra de B612 y B612 Mono. El mismo pangrama inglés, “quick brown fox,” es escrito en líneas separadas.
 | 
			
		||||
      "
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
Recientemente, redescubrí la fuente [B612][b612]. Fue engargado por AirBus para
 | 
			
		||||
los tableros de instrumentos de sus aviones. Hace unos años que hizen de código
 | 
			
		||||
abierto.
 | 
			
		||||
 | 
			
		||||
{{% figures/image name=specimen class=content-width shouldResize=false %}}
 | 
			
		||||
 | 
			
		||||
[b612]: https://b612-font.com
 | 
			
		||||
| 
						 | 
				
			
			@ -1,24 +0,0 @@
 | 
			
		|||
---
 | 
			
		||||
title: B612
 | 
			
		||||
date: 2024-03-09T08:38:03-08:00
 | 
			
		||||
description: An open source font I rediscovered recently.
 | 
			
		||||
categories: Tech
 | 
			
		||||
tags: [Fonts]
 | 
			
		||||
resources:
 | 
			
		||||
  - name: specimen
 | 
			
		||||
    src: specimen.png
 | 
			
		||||
    title: B612 and B612 Mono
 | 
			
		||||
    params:
 | 
			
		||||
      alt: "
 | 
			
		||||
      A specimen of B612 and B612 Mono. The same “quick brown fox” pangram sentence is
 | 
			
		||||
      set in each font on separate lines.
 | 
			
		||||
      "
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
I recently rediscovered [B612][b612], a font comissioned by AirBus to serve as
 | 
			
		||||
the font for their airplanes' instrument panels. They made it open source
 | 
			
		||||
several years ago.
 | 
			
		||||
 | 
			
		||||
{{% figures/image name=specimen class=content-width shouldResize=false %}}
 | 
			
		||||
 | 
			
		||||
[b612]: https://b612-font.com
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 150 KiB  |