add theming support

replace CSS BG grid with SVG-based version
This commit is contained in:
Eric Woodward 2024-04-27 15:37:02 -04:00
parent 751e201f18
commit c4d3da4dc2
7 changed files with 473 additions and 71 deletions

View File

@ -1,6 +1,6 @@
{
"name": "iew-site",
"version": "0.12.0",
"version": "0.13.0",
"description": "",
"main": "index.js",
"scripts": {},

View File

@ -0,0 +1,14 @@
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="smallGrid" width="20" height="20" patternUnits="userSpaceOnUse">
<rect width="18" height="18" fill="black" stroke="transparent"/>
</pattern>
<pattern id="grid" width="200" height="200" patternUnits="userSpaceOnUse">
<rect width="200" height="200" fill="url(#smallGrid)" />
<path d="M 200 0 L 0 0 0 200" fill="none" stroke="black" stroke-width="4`" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#smallGrid)" />
</svg>

After

Width:  |  Height:  |  Size: 608 B

View File

@ -61,6 +61,71 @@
});
}
// TODO: maybe put toggle behind collapsable "settings" menu that
// lives on bottom left of screen on mobile?
const actionBox = document.querySelector("#footer .actionBox");
/** THEME SWITCHER */
// load last theme
const currentTheme = Cookies.get("currentTheme") ?? "";
const themeSwitch = document.createElement("div");
themeSwitch.innerHTML = [
'<label class="themeSwitch" for="themeSwitch">',
'<div class="themeSwitch-description">',
"Theme",
"</div>",
'<select id="themeSwitch" name="themeSwitch">',
`<option value="" ${
currentTheme === "" ? "selected" : ""
}>Default</option>`,
`<option value="themeAdmiral" ${
currentTheme === "themeAdmiral" ? "selected" : ""
}>Admiral</option>`,
`<option value="themeAntsy92" ${
currentTheme === "themeAntsy92" ? "selected" : ""
}>Antsy92</option>`,
`<option value="themeBingo" ${
currentTheme === "themeBingo" ? "selected" : ""
}>Bingo</option>`,
`<option value="themeCyber77" ${
currentTheme === "themeCyber77" ? "selected" : ""
}>Cyber77</option>`,
`<option value="themeLightCyan" ${
currentTheme === "themeLightCyan" ? "selected" : ""
}>LightCyan</option>`,
`<option value="themeTerminalGreen" ${
currentTheme === "themeTerminalGreen" ? "selected" : ""
}>Terminal Green</option>`,
`<option value="themeVagabond" ${
currentTheme === "themeVagabond" ? "selected" : ""
}>Vagabond</option>`,
"</label>",
].join("\n");
themeSwitch.classList.add("js-themeSwitch", "themeSwitch");
if (currentTheme)
document
.getElementsByTagName("body")[0]
.classList.add(currentTheme);
actionBox.append(themeSwitch);
// add toggle event
document
.getElementById("themeSwitch")
.addEventListener("change", function (e) {
const body = document.getElementsByTagName("body")[0];
body.className = body.className
.split(/\s/)
.filter((val) => !val.startsWith("theme"))
.concat(e.target.value)
.join(" ");
Cookies.set("currentTheme", e.target.value);
});
/** SCROLLING BACKGROUND */
// default to no scrolling on devices under 600 px w
const isMobile = window.matchMedia(
"only screen and (max-width: 600px)"
@ -74,22 +139,18 @@
const scrollToggle = document.createElement("div");
scrollToggle.innerHTML = [
'<label class="toggleSwitchV3" for="scrollingToggle">',
'<div class="toggleSwitchV3-description">',
'<label class="toggleSwitch" for="scrollingToggle">',
'<div class="toggleSwitch-description">',
"Background",
"</div>",
`<input class="toggleSwitchV3-checkbox" ${
`<input class="toggleSwitch-checkbox" ${
hasScrollToggle ? "checked" : ""
} type="checkbox" id="scrollingToggle" />`,
'<div class="toggleSwitchV3-status" data-ts-on="SCROLLING" data-ts-off="STATIC"></div>',
'<div class="toggleSwitch-status" data-ts-on="SCROLLING" data-ts-off="STATIC"></div>',
"</label>",
].join("\n");
scrollToggle.classList.add("js-scrollToggle", "scrollToggle");
const actionBox = document
// .getElementById("footer")
// .querySelector("#footer > .pageFooter-inner")
.querySelector("#footer .actionBox");
actionBox.append(scrollToggle);
actionBox.classList.add("js-actionBox");
@ -113,10 +174,6 @@
.getElementById("gridContainer")
.classList.add("js-isAnimated");
// TODO: maybe put toggle behind collapsable "settings" menu that
// lives on bottom left of screen on mobile?
// can eventually toggle color schemes here, too?
if (document.documentElement.className.indexOf("is404") > -1) {
document.getElementById("searchQuery").value =
window.location.pathname

View File

@ -7,30 +7,186 @@
* http://creativecommons.org/publicdomain/zero/1.
****************************************************************************/
/*
Palette
color: #9aa8bc
bg: #0d1852
link: 049c74
border: 25baba
big-font: 25baba
black: 040308
other: 094192
*/
:root {
--color-bg: #040308;
--color-bg-translucent: rgba(4, 3, 8, 0.9);
--color-button-border: #726f6a;
--color-grid: #04778f;
--color-main: #9aa8bc;
--color-header: #25baba;
--color-header-translucent: rgba(37, 171, 171, 0.3);
--color-link: #049c74;
--color-link-translucent: rgba(4, 156, 116, 0.8);
--color-menubar: #282c32;
--color-other-dark: #0d1852;
--color-other-dark-translucent: rgba(13, 24, 82, 0.5);
--color-other-light: #04778f;
--color-other-link: #1e6e58;
--color-pre-text: #ddd;
--color-wtf-update-footer: #423f4d; /* retired in favor of --color-menubar */
--color-wtf-embargoed: #151222 /* retired in favor of --color-other-dark-translucent */
}
body {
background: #040308; /* Old browsers */
background: #040308;
color: #9aa8bc;
color: var(--color-main);
font-family: sans-serif;
font-size: 100%;
line-height: 1.5em;
}
body.themeAdmiral {
--color-bg: #3000FF;
--color-bg-translucent: rgba(48, 0, 255, .8);
--color-button-border: #ecfbff;
--color-grid: #ecfbff;
--color-header: #ecfbff;
--color-header-translucent: rgba(236, 251, 255, .3);
--color-link: #79BAEC;
--color-link-translucent: rgba(236, 251, 255, .8);
--color-main: #e6f4f1;
--color-menubar: rgba(236, 251, 255, .1);
--color-other-light: #ecfbff;
--color-other-link: #79BAEC;
}
body.themeAntsy92 {
--color-bg: #000;
--color-bg-translucent: rgba(0, 0, 0, .7);
--color-button-border: #0cc;
--color-grid: #00c;
--color-header: #c0c;
--color-header-translucent: rgba(204, 0, 204, .3);
--color-link: #0cc;
--color-link-translucent: rgba(0, 204, 204, .8);
--color-main: #fff;
--color-menubar: rgba(204, 0, 0, .2);
--color-other-light: #00c;
--color-other-link: #0c0;
background: var(--color-bg);
}
body.themeAntsy92 .menubar {
background-color: var(--color-bg);
border: 1px solid var(--color-grid);
border-left: none;
border-right: none;
}
body.themeBingo {
--color-bg: #1C71C6;
--color-bg-translucent: rgba(28, 113, 198, .9);
--color-button-border: #ecfbff;
--color-grid: #ecfbff;
--color-header: #ecfbff;
--color-header-translucent: rgba(236, 251, 255, .3);
--color-link: #79BAEC;
--color-link-translucent: rgba(236, 251, 255, .8);
--color-main: #e6f4f1;
--color-menubar: rgba(236, 251, 255, .1);
--color-other-light: #ecfbff;
--color-other-link: #79BAEC;
}
body.themeCyber77 {
/* Based on https://github.com/endormi/vscode-2077-theme/ */
--color-bg: #030d22;
--color-bg-translucent: rgba(3, 13, 34, .8);
--color-button-border: #c832ff;
--color-grid: #e92778;
--color-header: #39C0FF;
--color-header-translucent: rgba(57, 192, 255, .3);
--color-link: #ff2e97;
--color-link-translucent: rgba(233, 29, 120, .8);
--color-main: #e6f4f1;
--color-menubar: rgba(236, 251, 255, .1);
--color-other-light: #e92778;
--color-other-link: #ff2cf1;
}
body.themeLightCyan {
/* Based on a technique described at https://web.dev/articles/building/a-color-scheme */
--brand: #049c74;
--brand-hue: 164;
--brand-saturation: 95%;
--brand-lightness: 31%;
--color-bg: hsl(var(--brand-hue), 25%, 90%);
--color-bg-translucent: hsla(var(--brand-hue), 25%, 90%, .9);
--color-button-border: hsl(var(--brand-hue), var(--brand-saturation), 25%);
--color-grid: hsl(var(--brand-hue), var(--brand-saturation), 10%);
--color-header: hsl(var(--brand-hue), var(--brand-saturation), 25%);
--color-header-translucent: hsla(var(--brand-hue), var(--brand-saturation), 10%, .3);
--color-link: hsl(var(--brand-hue), var(--brand-saturation), var(--brand-lightness));
--color-link-translucent: hsla(var(--brand-hue), var(--brand-saturation), var(--brand-lightness), .8);
--color-main: hsl(var(--brand-hue), var(--brand-saturation), 10%);
--color-menubar: hsla(var(--brand-hue), 20%, 40%, .2);
--color-other-link: #1e6e58;
--color-other-light: hsl(var(--brand-hue), 30%, 30%);
--color-shadow: hsla(var(--brand-hue), 10%, 20%, .3);
}
body.themeLightCyan .siteTitle-link {
text-shadow: 0px 1px 1px var(--color-shadow);
}
body.themeLightCyan .siteTitle-link:hover {
text-shadow: 0px 2px 2px var(--color-shadow);
}
body.themeTerminalGreen {
--color-bg: #040308;
--color-bg-translucent: rgba(3, 3, 3, .8);
--color-button-border: #a3af9e;
--color-grid: #009600;
--color-header: #f0fdec;
--color-header-translucent: rgba(0, 194, 167, .3);
--color-link: #009600;
--color-link-translucent: rgba(0, 150, 0, .8);
--color-main: #dff8d5;
--color-menubar: rgba(236, 236, 236, .1);
--color-other-light: #009600;
--color-other-link: #00964b;
}
body.themeVagabond {
--color-bg: #2C0F2A;
--color-bg-translucent: rgba(44, 15, 42, .8);
--color-button-border: #ecfbff;
--color-grid: #E94E5C;
--color-header: #E94E5C;
--color-header-translucent: rgba(233, 78, 92, .3);
--color-link: #44FCFC;
--color-link-translucent: rgba(68, 252, 252, .8);
--color-main: #FADBB0;
--color-menubar: rgba(236, 251, 255, .1);
--color-other-light: #F6BC43;
--color-other-link: #CC45B6;
}
body.themeVagabond .siteTitle-link:visited {
color: var(--color-link);
}
body.themeVagabond .navMenu-list-link:visited {
color: var(--color-link);
}
body.themeVagabond .navMenu-list-link.isCurrentSection {
color: var(--color-bg);
}
a {
border: 1px solid transparent;
border-bottom: 1px dashed #25baba;
border-bottom-color: var(--color-header);
color: #049c74;
color: var(--color-link);
font-weight: bold;
padding: 0 0.2rem;
text-decoration: none;
@ -45,18 +201,23 @@ a[target="_blank"]::after {
a:hover {
background-color: #25baba;
background-color: var(--color-header);
border: 1px solid #25baba;
border-color: var(--color-header);
border-radius: 0.3rem;
color: #040308;
color: var(--color-bg);
text-decoration: none;
}
a:hover:visited {
color: #040308;
color: var(--color-bg);
}
a:visited {
color: #1e6e58;
color: var(--color-other-link);
}
article img {
@ -65,6 +226,7 @@ article img {
blockquote {
border-left: 2px solid #04778f;
border-left-color: var(--color-other-light);
font-style: italic;
margin: 1em 0;
padding: 0 1em;
@ -74,7 +236,9 @@ code,
kbd {
background-color: #0d1852;
background-color: rgba(13, 24, 82, 0.5);
color: #ccc;
background-color: var(--color-other-dark-translucent);
color: #ddd;
color: var(--color-pre-text);
font-size: 0.9em;
padding: 0.25em;
}
@ -113,36 +277,42 @@ figure[data-type="video"] video {
h1 {
color: #25baba;
color: var(--color-header);
font-size: 2em;
line-height: 1.2em;
}
h2 {
color: #25baba;
color: var(--color-header);
font-size: 1.5em;
line-height: 1.2em;
}
h3 {
color: #25baba;
color: var(--color-header);
font-size: 1.22em;
line-height: 1.2em;
}
h4 {
color: #25baba;
color: var(--color-header);
font-size: 1.12em;
line-height: 1.2em;
}
h5 {
color: #25baba;
color: var(--color-header);
font-size: 1.06em;
line-height: 1.2em;
}
h6 {
color: #25baba;
color: var(--color-header);
font-size: 1em;
}
@ -174,7 +344,9 @@ ul {
pre {
background-color: #0d1852;
background-color: rgba(13, 24, 82, 0.5);
color: #ccc;
background-color: var(--color-other-dark-translucent);
color: #ddd;
color: var(--color-pre-text);
font-size: 0.9em;
overflow: auto;
padding: 0.25em;
@ -200,6 +372,7 @@ samp {
.actionBox.js-actionBox > .topLink {
align-items: center;
border: 1px solid #726f6a;
border-color: var(--color-button-border);
border-radius: 0.3em;
display: flex;
flex-wrap: wrap;
@ -317,11 +490,14 @@ samp {
.asideContent {
border-top: 1px dotted #04778f;
border-top-color: var(--color-other-light);
}
.asideMenu-divider {
border: 1px dashed #25baba;
border-color: var(--color-header);
color: #25baba;
color: var(--color-header);
width: 80%;
}
@ -334,6 +510,7 @@ samp {
.asideMenu-link {
border: 1px solid #726f6a;
border-color: var(--color-button-border);
border-radius: 0.3em;
display: inline-block;
padding: 0.25em;
@ -391,13 +568,16 @@ samp {
.backLink-link {
border: 1px solid #25baba;
border-color: var(--color-header);
color: #25baba;
color: var(--color-header);
display: inline-block;
padding: 0.25em 0.5em;
}
.boxLink {
border: 1px solid #726f6a;
border-color: var(--color-button-border);
border-radius: 0.3em;
display: inline-block;
padding: 0.25em;
@ -406,34 +586,43 @@ samp {
.boxLink.isCurrent {
background-color: #049c74;
background-color: rgba(4, 156, 116, 0.8);
background-color: var(--color-link-translucent);
color: #040308;
color: var(--color-bg);
text-decoration: none;
}
.boxLink.isCurrent:hover {
background-color: #25baba;
background-color: var(--color-header);
}
.container {
color: white;
color: #ddd;
color: var(--color-pre-text);
}
.dataTable td {
border-left: 1px solid #9aa8bc;
border-left-color: var(--color-main);
border-right: 1px solid #9aa8bc;
border-right-color: var(--color-main);
padding: 0.1em 0.2em;
text-align: center;
}
.dataTable th {
border: 1px solid #9aa8bc;
border-color: var(--color-main);
color: #25baba;
color: var(--color-header);
padding: 0.2em 0.4em;
text-align: center;
}
.dataTable thead tr {
background-color: #0d1852;
background-color: var(--color-other-dark);
}
.dataTable tr {
@ -443,16 +632,21 @@ samp {
.dataTable tbody tr:last-child {
border-bottom: 1px solid #9aa8bc;
border-bottom-color: var(--color-main);
}
.dataTable tbody:nth-child(even) tr:nth-child(even) {
background-color: #0d1852;
background-color: var(--color-other-dark);
}
.dataTable tr:hover td {
border-bottom: 1px solid #25baba;
border-bottom-color: var(--color-header);
border-top: 1px solid #25baba;
border-top-color: var(--color-header);
color: #25baba;
color: var(--color-header);
}
.dirList {
@ -519,6 +713,7 @@ samp {
.feedLine {
border-color: #25baba;
border-color: rgba(37, 171, 171, 0.3);
border-color: var(--color-header-translucent);
clear: both;
margin: 2.5em auto;
width: 80%;
@ -549,8 +744,10 @@ samp {
-webkit-background-clip: content-box;
background-position: center bottom;
background-size: 20px 20px;
background-image: linear-gradient(to right, #04778f 2px, transparent 2px), linear-gradient(to bottom, #04778f 2px, transparent 2px);
background-color: var(--color-grid);
background-image: url('/images/grid.svg');
bottom: 0;
contain: paint;
content: "";
display: block;
height: 100vh;
@ -621,7 +818,9 @@ samp {
.linkButton-link {
border: 1px solid #25baba;
border-color: var(--color-header);
color: #25baba;
color: var(--color-header);
display: inline-block;
padding: 0.25em 0.5em;
}
@ -668,6 +867,7 @@ samp {
.menubar {
background: #282c32;
background-color: var(--color-menubar);
display: static;
}
@ -690,18 +890,23 @@ samp {
.navMenu-list-link:visited {
color: #25baba;
color: var(--color-header);
}
.navMenu-list-link.isCurrentSection {
background-color: #049c74;
background-color: var(--color-link);
border: 1px solid #25baba;
border-color: var(--color-header);
border-radius: 0.3rem;
color: #040308;
color: var(--color-bg);
text-decoration: none;
}
.navMenu-list-link.isCurrentSection:hover {
background-color: #25baba;
background-color: var(--color-header);
}
.navMenu-list-item {
@ -720,7 +925,9 @@ samp {
.page {
background: #040308;
background: rgba(4, 3, 8, 0.9);
background: var(--color-bg-translucent);
border: 1px solid #9aa8bc;
border-color: var(--color-main);
border-top: none;
border-bottom: none;
display: block;
@ -734,6 +941,7 @@ samp {
.pageFooter {
border-top: 1px solid #04778f;
border-top-color: var(--color-other-light);
margin: 0 auto;
padding: 0.5em 1em;
width: 100%;
@ -825,6 +1033,7 @@ samp {
.pageMenu-item:hover {
border-color: #25baba;
border-color: var(--color-header);
}
.pageMenu-link {
@ -837,7 +1046,9 @@ samp {
.pageMenu-link:hover .pageMenu-text {
background-color: #25baba;
background-color: rgba(150, 150, 150, 0.8);
background-color: var(--color-header-translucent);
color: #040308;
color: var(--color-bg);
text-decoration: none;
width: 100%;
}
@ -892,6 +1103,7 @@ samp {
.postMenu-link {
border: 1px solid #726f6a;
border-color: var(--color-button-border);
border-radius: 0.3em;
display: inline-block;
padding: 0.25em;
@ -955,8 +1167,10 @@ samp {
.searchBox-query {
background: #040308;
background: rgba(4, 3, 8, 0.9);
background: var(--color-bg-translucent);
border-radius: 0.3rem;
color: #ddd;
color: var(--color-pre-text);
width: 10em;
}
@ -969,6 +1183,7 @@ samp {
border: 1px solid transparent;
border-radius: 1rem;
color: #25baba;
color: var(--color-header);
display: inline-block;
font-style: normal;
padding: 0.2em;
@ -983,18 +1198,7 @@ samp {
.siteTitle-link:visited {
color: #25baba;
}
.spellTable {
margin-left: -1em;
}
.spellTable td {
border: 1px solid #094192;
}
.spellTable tbody:nth-child(even) {
background-color: #0d1852;
color: var(--color-header);
}
.tagPage-counts {
@ -1021,10 +1225,19 @@ samp {
.teaseLink-link {
color: #25baba;
color: var(--color-header);
display: inline-block;
padding: 0.25em 0.5em;
}
.themeSwitch select {
background-color: transparent;
border: 1px solid #726f6a;
border-color: var(--color-button-border);
color: #049c74;
color: var(--color-link);
}
.titleLink {
border-bottom: none;
}
@ -1032,6 +1245,7 @@ samp {
.titleLink:active,
.titleLink:hover {
border: 1px solid #25baba;
border-color: var(--color-header);
}
.toggleBox {
@ -1040,46 +1254,49 @@ samp {
justify-content: end;
}
.toggleSwitchV3 {
.toggleSwitch {
border: 1px solid #726f6a;
border-color: var(--color-button-border);
border-radius: 0.3em;
}
.toggleSwitchV3-checkbox {
.toggleSwitch-checkbox {
display: none;
}
.toggleSwitchV3-checkbox::selection,
.toggleSwitchV3-checkbox::-moz-selection,
.toggleSwitchV3-checkbox:after::selection,
.toggleSwitchV3-checkbox:after::-moz-selection,
.toggleSwitchV3-checkbox:before::selection,
.toggleSwitchV3-checkbox:before::-moz-selection,
.toggleSwitchV3-checkbox *::selection,
.toggleSwitchV3-checkbox *::-moz-selection,
.toggleSwitchV3-checkbox *:after::selection,
.toggleSwitchV3-checkbox *:after::-moz-selection,
.toggleSwitchV3-checkbox *:before::selection,
.toggleSwitchV3-checkbox *:before::-moz-selection,
.toggleSwitchV3-checkbox + .toggleSwitchV3-status::selection,
.toggleSwitchV3-checkbox + .toggleSwitchV3-status::-moz-selection {
.toggleSwitch-checkbox::selection,
.toggleSwitch-checkbox::-moz-selection,
.toggleSwitch-checkbox:after::selection,
.toggleSwitch-checkbox:after::-moz-selection,
.toggleSwitch-checkbox:before::selection,
.toggleSwitch-checkbox:before::-moz-selection,
.toggleSwitch-checkbox *::selection,
.toggleSwitch-checkbox *::-moz-selection,
.toggleSwitch-checkbox *:after::selection,
.toggleSwitch-checkbox *:after::-moz-selection,
.toggleSwitch-checkbox *:before::selection,
.toggleSwitch-checkbox *:before::-moz-selection,
.toggleSwitch-checkbox + .toggleSwitch-status::selection,
.toggleSwitch-checkbox + .toggleSwitch-status::-moz-selection {
background: none;
}
.toggleSwitchV3-checkbox:checked + .toggleSwitchV3-status {
.toggleSwitch-checkbox:checked + .toggleSwitch-status {
background: #1e6e58;
background: var(--color-other-link);
}
.toggleSwitchV3-checkbox:checked + .toggleSwitchV3-status:before {
.toggleSwitch-checkbox:checked + .toggleSwitch-status:before {
bottom: -100%;
}
.toggleSwitchV3-checkbox:checked + .toggleSwitchV3-status:after {
.toggleSwitch-checkbox:checked + .toggleSwitch-status:after {
bottom: 0;
}
.toggleSwitchV3-description {
.toggleSwitch-description {
color: #049c74;
color: var(--color-link);
font-weight: bold;
padding: .5rem;
text-decoration: none;
@ -1087,7 +1304,7 @@ samp {
-webkit-transition: 0.3s background-color, 0.3s color, 0.3s border-radius;
}
.toggleSwitchV3-status {
.toggleSwitch-status {
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
background: transparent;
@ -1107,9 +1324,10 @@ samp {
width: 100%;
}
.toggleSwitchV3-status:after,
.toggleSwitchV3-status:before {
.toggleSwitch-status:after,
.toggleSwitch-status:before {
color: #049c74;
color: var(--color-link);
content: "";
display: inline-block;
font-weight: bold;
@ -1121,16 +1339,18 @@ samp {
width: 100%;
}
.toggleSwitchV3-status:after {
.toggleSwitch-status:after {
border-top: 1px solid transparent;
bottom: 100%;
color: #040308;
color: var(--color-bg);
content: attr(data-ts-on);
left: 0;
}
.toggleSwitchV3-status:before {
.toggleSwitch-status:before {
border-top: 1px solid #726f6a;
border-top-color: var(--color-button-border);
bottom: 0;
content: attr(data-ts-off);
left: 0;
@ -1138,13 +1358,16 @@ samp {
/* hover effects only on devices with fine pointers (AKA not phones) */
@media(hover: hover) and (pointer: fine) {
.toggleSwitchV3:hover {
border: 1px solid hsl(180, 67%, 44%);
.toggleSwitch:hover {
border: 1px solid #25baba;
border-color: var(--color-header);
}
.toggleSwitchV3:hover > .toggleSwitchV3-description {
.toggleSwitch:hover > .toggleSwitch-description {
background-color: #25baba;
background-color: var(--color-header);
color: #040308;
color: var(--color-bg);
text-decoration: none;
}
}
@ -1158,6 +1381,7 @@ samp {
.topLink {
border: 1px solid #726f6a;
border-color: var(--color-button-border);
border-radius: 0.3em;
display: block;
margin: 2em auto;
@ -1167,6 +1391,7 @@ samp {
}
.topLink:visited {
color: #049c74;
color: var(--color-link);
}
.twitter-tweet {
@ -1181,6 +1406,7 @@ samp {
.update-footer {
align-items: start;
color: #423f4d;
color: var(--color-menubar);
display: flex;
flex-direction: column;
font-size: 0.9em;
@ -1206,6 +1432,7 @@ samp {
.update-nav-link {
border: 1px dashed #726f6a;
border-color: var(--color-button-border);
border-radius: 0.3em;
display: block;
padding: 0.5em;
@ -1251,12 +1478,15 @@ samp {
.asideMenu-link.isCurrent {
background-color: #049c74;
background-color: rgba(4, 156, 116, 0.8);
background-color: var(--color-link-translucent);
color: #040308;
color: var(--color-bg);
text-decoration: none;
}
.asideMenu-link.isCurrent:hover {
background-color: #25baba;
background-color: var(--color-header);
}
.asideUpdates .update-citation {
@ -1278,8 +1508,10 @@ samp {
.feature hr {
border: 1px 0 0 0;
border-color: #0d1852;
border-color: var(--color-other-dark);
border-style: solid;
color: #0d1852;
color: var(--color-other-dark);
max-width: 90%;
}
@ -1290,31 +1522,39 @@ samp {
.pageMenu-link.isCurrent .pageMenu-text {
background-color: #049c74;
background-color: rgba(4, 156, 116, 0.8);
background-color: var(--color-link-translucent);
color: #040308;
color: var(--color-bg);
text-decoration: none;
}
.pageMenu-link.isCurrent:hover .pageMenu-text {
background-color: #25baba;
background-color: var(--color-header);
}
.textMenu-link.isCurrent {
background-color: #049c74;
background-color: rgba(4, 156, 116, 0.8);
background-color: var(--color-link-translucent);
color: #040308;
color: var(--color-bg);
text-decoration: none;
}
.textMenu-link.isCurrent:hover {
background-color: #25baba;
background-color: var(--color-header);
}
.update.isDraft {
background-color: #282c32;
background-color: var(--color-menubar);
}
.update.isEmbargoed {
background-color: #151222;
background-color: var(--color-other-dark-translucent);
}
/****************************************************************************
@ -1371,7 +1611,9 @@ samp {
.asideContent {
background: #040308;
background: rgba(4, 3, 8, 0.9);
background-color: var(--color-bg-translucent);
border: 1px solid #9aa8bc;
border-color: var(--color-main);
font-size: 0.8em;
position: absolute;
top: 5em;

View File

@ -100,7 +100,6 @@
This site is powered by <a href="https://nodejs.org/">Node</a>,
<a href="https://nginx.org/">nginx</a>,
<a href="https://vscodium.com/">VSCodium</a>,
<a href="https://www.pexels.com/photo/programming-427722/">this image from EMIL Ivanov at PEXELS</a>,
<a href="https://www.google.com/fonts/specimen/Exo+2">the Exo 2 font</a>,
<a href="https://www.raspberrypi.com/products/raspberry-pi-4-model-b/">a Raspberry Pi 4</a>, and
<a rel="self" href="#aboutme">a truly massive ego</a>.

View File

@ -3,7 +3,7 @@ title: "Site Update: Welcome to the Grid!"
content_type: journal
date_pub: 2024-04-09T22:17:45.000-04:00
description: Some details around the recent addition of a scrolling background grid to the site.
tags: cyberpunk code interwebs ProgressiveEnhancement WebDev
tags: cyberpunk code interwebs WebDev
---
Recently, I decided to try my hand at some CSS shenanigans, and spent a few hours replacing this site's long-serving background image with a scrolling grid background.
@ -23,3 +23,13 @@ Except... I know that not everyone _likes_ moving background effects, so the onl
If you want to know more specifics, check out the [commit](https://git.itsericwoodward.com/eric/itsericwoodward-site-v2/commit/b2d2fb6d34f1ec14c3c9b7349b87559eddb8f880) on my [self-hosted git server](https://git.itsericwoodward.com/eric/), in particular the changes to the [`scripts.js`](https://git.itsericwoodward.com/eric/itsericwoodward-site-v2/commit/b2d2fb6d34f1ec14c3c9b7349b87559eddb8f880#diff-295d89f33ba7c5c5879162f1cd37fd4c2a68c7d1) and [`styles.css`](https://git.itsericwoodward.com/eric/itsericwoodward-site-v2/commit/b2d2fb6d34f1ec14c3c9b7349b87559eddb8f880#diff-e5a74efc37d0925d2ad32f86388d0229678ed49f) files.
Because I, too, fight for the users.
---
_Update: 2024-04-27_
Since posting this, I've come to realize that the CSS-based grid solution I described above had some problems - most notably, the scrolling-performance hit, particularly on long pages. The simple presence of the affect was causing clipping issues and serious scroll-lag _all over the site_ (really, any page longer than one vertical screen). Plus, the animations themselves had a tendency to get super-janky the longer they went.
Not as "user"-empowering as I'd hoped. :(
In the end, I wound up trading out my pure CSS solution for an implementation that uses a [tiny SVG file](/images/grid.svg), as it is _much_ more performant, while still allowing for color customization (I wonder why... :thinking:). It's a little bit "flashy", but it's still a decided improvement.

View File

@ -0,0 +1,80 @@
---
title: One Night, on White Plume Mountain...
content_type: journal
date_pub: 2024-04-27T10:13:40.000-04:00
description: An overview of a recent one-shot RPG I ran, based on the classic module.
tags: DnD games OSR PlanarVagabond RPG
---
A few weeks ago, I had the good fortune to run a one-shot version of Lawrence Schick's classic AD&D adventure [White Plume Mountain](https://en.wikipedia.org/wiki/White_Plume_Mountain) for some friends and family, and I thought I do a bit of a post-mortem on it here.
### Background
For context, this was my second attempt at running a classic module in a single session, the first one being my Halloween game from last year, _One Night in Ravenloft_. In that game, a party of adventurers (mostly made up of my old 5E group) practically strolled through Strahd's castle, slaughtering everything that tried to stop them, before steamrolling the vampire lord himself. Heck, the [automaton](https://www.planarvagabond.com/races/automaton.html) fighter "Gus the Party Bus" even stole a piece of Strahd's iconic organ to give himself a "toot-toot" horn. It was a _cakewalk_.
So it was that mostly the same group signed up for [One Night in White Plume Mountain](https://www.planarvagabond.com/campaigns/one-night/on-white-plume-mountain.html). The idea was to run a 4-5 hour version of the module where the PCs have been hired to retrieve the 3 lost items from the wizard's dungeon. Then, to put an extra time pressure on the game, I made the volcano somewhat more active than usual, intimating that they needed to get out before it blows (intended to happen at the "hard stop" time for the session, about 4.5 hours from the intended start time).
As per usual for my games, I ran it in Roll20 (with voice chat / memeposting via Discord), and since I had already purchased them, I used the excellent maps from the [Tales from the Yawning Portal](https://marketplace.roll20.net/browse/bundle/3804/dandd-tales-from-the-yawning-portal) pack. Also as per usual, we used my custom B/X-inspired [heartbreaker](https://rpgmuseum.fandom.com/wiki/Fantasy_heartbreaker) ruleset, [HOSR](https://www.planarvagabond.com/rules/index.html).
I made several changes to the base module, "renaming" (and lightly re-theming) several of the named NPCs (ex: Burket the fighter became Bur-Ket the barbarian, Snarla the werewolf magic-user became Snar'la the werewolf [bloodmage](https://www.planarvagabond.com/classes/bloodmage.html)). I also changed the missing artifacts slightly, rebuilding them based on some ideas I'd been toying with from my astral campaign (I don't particularly care for the "classic" rules for sentient weapons, but using elementals...). Of course, none of it got used in the actual _session_, but how was I to know...
One issue the first game ran into was not having all of the characters ready by the start time, so the session started early for those that still needed to finish their character builds. As we were using HOSR, the [character creation](https://www.planarvagabond.com/rules/basics.html#character-creation) was pretty quick, so by the start time (4.5 hours until deadline), all of the characters were ready with the exception of their _GM gift_: a random magic item culled from various sources (primarily [OSE Advanced Fantasy](https://necroticgnome.com/products/old-school-essentials-advanced-fantasy-referees-tome)). I usually have the list ready ahead of time, but it was one of the parts I hadn't gotten around to yet, so.... 30 minutes burned before we even _started_. :facepalm:
Finally, with just 4 hours left on the clock, the adventurers stepped into the dungeon.
### Entering the Dungeon
To start out, I filled them in on some backstory, both from the adventure (the poem) and with my own twists on top. The fire priests warned of the impending eruption, and the white plume was turning yellow, so the eruption ws well-telegraphed. Additionally, they were blessed by one of the priests of Kud (the dwarven god of law associated with the missing hammer, **Wylm, the Eternal Rest**), who told them "as long as one remains, the quest may continue", and gave them a bag of holding loaded with healing potions and scrolls (most of which they never used).
Their first stop was the (rather soggy) gynosphinx in area #2. Unfortunately for them, their reaction roll with her was a 3, which usually means a combat encounter, but since she was just "doing a job" (and based on some suggestions I found online), I played her as the most disinterested teenager I could, giving the party _zero_ help (despite their frequent attempts at RP-ing something out of her). After much hemming-and-hawing, they chose the central hallway.
Finding the drain room (area #9), they decided to cast light on the party's sole automaton, the thief Dungeon Ken. This turned out to be a good move on their part, as he continued to glow brightly over the rest of the session, serving as the requisite illumination for all of the darkvision-less party members. They found and turned the crank, starting the long process of draining, before continuing down the hallway.
The next room was the kelpie pool (area #10), which, to my mind, has to be one of the oddest encounters in the adventure: a pair shape-shifting seaweed women who can each cast charm only once per day and who use this ability to lure adventurers into the water so they can drown themselves. As far as I can tell, in the adventure-as-written, if said adventurer fails a lone **Save vs Spells**, they jump in the water and immediately start drowning (2d10 damage per round). This seemed... unfair, to say the least. So when Tarin, the dwarven thief, failed his roll and jumped in, I decided to treat it like a death save: he had to make **Save vs Death** each round to prevent himself from [drowning](https://www.planarvagabond.com/rules/adventuring.html#drowning) (taking 1d8 damage per round).
The other weird thing about the kelpies is that there's only two of them. In an adventure designed for 6-11 players, having at most 2 PCs cursed in such a way just seemed like an odd decision (almost as odd as not even giving them a simple claw attack or something to fall back on). Still, with one PC drowning and most of the rest afraid to jump in the water (lest they drown as well), this sequence took a while before mercifully coming to end (although the still-glowing Dungeon Ken was useful for finding the kelpies' treasure).
With 2 hours left to go, the party reached the **Spinning Cylinder of Doom** (room #11). On a whim, Dungeon Ken ran and slid across the tunnel, safely landing on the other side, and encouraging others to do the same. This meant that, by time Bur-Ket tried to fire his flaming arrow, he already had 3 adventurers right next to him, including the tea-obsessed dwarven mystic Iroh, who (thanks to the Sword of Quickness obtained as a pre-game GM gift) managed to leap in front of the flaming arrow, preventing it from setting the rest of the group aflame.
As the rest of the party slid across the tunnel, those who had already made the trip picked the lock on the door to area #12 and busted in, taking on the bloodmage and her barbarian boyfriend. Tarin managed a [critical hit](https://www.planarvagabond.com/rules/combat.html#critical-hits) with his dual light hammers, and before long, Bur-Ket was dead. This enraged his girlfriend, who promptly turned into a werewolf. Unfortunately for her, she was stopped by Kallor Zeph, the party's nihilistic human cleric of Xar'Kos, who cast darkness on her eyes, blinding her, and giving the rest of the party ample time to finish her off.
Searching the dead couple's inner sanctum in area #13, the party was disheartened to find only gems and coins, and no trace of any of the weapons they had come for. They knew time was running out (only about 45 minutes remained until the end of the session), so they rushed through the double doors (which had already been somewhat explored by Kallor, the human wizard Okalis "Okey" Baker, and the giantkin barbarian Olive), and headed into battle with the Beast of the Boiling Lake in area #17.
The party (mostly) tried to draw the Huge Giant Crab (TM) away from the treasure in hopes that one of them could go grab it. Olive, in particular, took the beast head-on, wounding it some, but taking 3 claw hits in quick succession for her trouble, dropping her to 1 HP. At this point, Dungeon Ken decided to cast Wall of Stone from a scroll he had received as a pregame GM gift, wrapping the HGC in a 2' solid stone wall. As we were already 5 minutes past the cut-off time, the party grabbed the trident **Wha'yve, the Flood** just as the volcano came to life, so they ran back down the hallway, headed back out the way they came, and made it out of the volcano with one of the three artifacts.
Or did they? I'll get back to that.
### Results
So, how did they fair overall? If I had to award XP for their misadventures, I'd say:
- They solved the sphinx's riddle: 650 XP (for bypassing her)
- They beat the kelpies and found their treasure: 175 x 2 (kelpies) + 600 (gold) + 2000 (necklace) = 2950 XP, and that doesn't include the suit of chain mail +3 that no one wanted (and thus they intended to sell).
- They defeated both Bur-Ket and Snar'la, and took their treasure: 175 (Bur-Ket) + 1250 (Snar'la) + 500 (gold) + 1300 (gems) = 3225 XP
- They trapped the Huge Giant Crab (TM) and took its treasure, including the trident: 1350 (HGC) + 1000 (gold) + 11,000(!) (gems) = 13,350 XP, and that doesn't count the magical goodies they would have gotten:
- **Ring of Infravision (60')**,
- **Luckstone**,
- **Wand of Frost**, and
- **Wha'yve, the Flood**: a magical trident with an elemental bound to it, and one of the three main artifacts they were looking for.
That brings it to 19,525 XP, divided among 6 PCs = approximately 3300 XP each.
### Retrospective
Looking back on the adventure, here are some key takeaways for me:
- As much as it pains me to admit it, it seems that running games with more than 4 or 5 players on Discord / Roll20 is prohibitively difficult (at least for me). Between the long pauses, talking over each other, and waiting to see who's going to make the first move, it took over 3 hours just to clear the first handful of rooms. I mean, I love getting everyone together like that, and some games work better for it than others, but I can't see trying to run another online RPG like this for more than 5, and might even aim for 4 on my next one (possibly running it multiple times for different groups, if there's enough interest).
- I should definitely have had some pre-rolled magic items. I probably could even have used Necrotic Gnome's own [magic item generator](https://oldschoolessentials.necroticgnome.com/generators/magic-item-generator) just to figure out options, as that ate a full 30 minutes out of the game - at least enough time for them to start on another hallway (and get to know **Wha'yve** a bit, which they didn't really).
- Because of how my last few months of games have lined up, I haven't run a good, old-fashioned #dungeoncrawl in quite some time. I forget sometimes that _this_ is where the older systems really shine, exploring darkened (and flooded) corridors room-to-room, listening at doors while checking for locks and traps and secrets...
As soon as we were done, some of the players expressed an interest in going back in and trying to grab the other 2 items. Part of me wants to do that, but since the volcano exploded at the end, I feel like it's not really a viable option without some type of "magical intervention", so I guess it's a good thing it's a _mad wizard's dungeon_...
I'm thinking about treating it like Groundhog's Day or Happy Death Day scenario: once they entered the dungeon, they got locked in a time-loop, and the only way to get out of it is to collect _all 3_ relics. Or maybe defeat the mad wizard Khyr-Aptis himself. Or maybe both! :D
But, yeah, I could definitely see running this adventure again, possibly with a few more (slight) tweaks.
If nothing else, I think I'll do another one of these _types_ of adventures soon. As I mentioned before, I have the _Tales from the Yawning Portal_ pack, and it includes maps for several iconic modules, so doing a _One Night in the [Forge of Fury](https://en.wikipedia.org/wiki/The_Forge_of_Fury)_ (or the _[Sunless Citadel](https://en.wikipedia.org/wiki/The_Sunless_Citadel)_) might be an option... Or maybe even the dreaded _[Tomb of Horrors](https://en.wikipedia.org/wiki/Tomb_of_Horrors)_?
Anyways, I'd like to thank my players (as always) for putting up with me, my inane rulings, bizarre ruleset requirements (and exclusions), and other GM-ing idiosyncrasies.
Until next time...