commit de259dba5b4c4eaf0532659c6edfcbc057e7ef10 Author: bs-sensei Date: Tue Mar 24 20:30:43 2026 -0400 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..2c854ce --- /dev/null +++ b/README.md @@ -0,0 +1,384 @@ +# Codex Obscura + +> *A dark academia pixel Hugo theme with a fall color palette.* +> +> Candlelit manuscripts meet cozy stationery. Books, tea, fountain pens, and the quiet rustle of digital marginalia. + +--- + +``` + ██████╗ ██████╗ ██████╗ ███████╗██╗ ██╗ +██╔════╝██╔═══██╗██╔══██╗██╔════╝╚██╗██╔╝ +██║ ██║ ██║██║ ██║█████╗ ╚███╔╝ +██║ ██║ ██║██║ ██║██╔══╝ ██╔██╗ +╚██████╗╚██████╔╝██████╔╝███████╗██╔╝ ██╗ + ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ +``` + +--- + +## Features + +- **Fall color palette** — deep ambers, burnt orange, rust, forest green, umber, parchment cream +- **Pixel art borders** — CSS-only pixel-perfect box shadow borders on all cards and widgets +- **Blinking `█` cursor** in the site title (optional, easily disabled) +- **Dark academia typography** — VT323 (pixel/terminal) for headings + Josefin Sans for body text +- **Dust mote ambient animation** — soft floating particles that drift across the page like dust in a library +- **Sidebar** — sys-info widget with live uptime counter, recent posts, tag cloud, and optional marginalia quote +- **Year-grouped archive** — posts grouped by year in terminal `ls -la` style +- **Tag filtering** — clickable tag chips at the top of the archive page with active highlighting +- **Post subtitles** — optional per-post tagline shown on cards and single pages +- **Responsive (Mostly)** — two-column desktop, stacked mobile +- **RSS feed** — full RSS out of the box +- **SEO-ready** — Open Graph, Twitter Card, canonical URLs +- **Syntax highlighting** — via highlight.js with Common Lisp support, add more lines to layout/partials/head.html for more +- **LaTeX rendering** — via KaTeX with Hugo passthrough for clean math expressions +- **Custom code blocks** — shortcode with language label and optional title +- **Comments** — Comentario integration (self-hosted, privacy-focused) + +--- + +## Installation + +### As a Git Submodule (recommended) + +First run `hugo new site ` + +```bash +cd +git submodule add https://git.johnabs.xyz/bs-sensei/codex-obscura-hugo-theme +``` + +### Manual + +Download and place the folder at `themes/codex-obscura/` inside your Hugo site, which you should have made using `hugo new site ` + +--- + +## Configuration + +After creating your site at from above and adding the template, you should first go to the `content` folder and create an about.md, resume.md, and a donate.md in your content folder or these will redirect to "Not Found". Your posts link will also redirect, unless you make a folder within content called `posts` and make `post1.md` inside it and add some text. Examples of each of these are provided below for your convenience. + +A full `hugo.toml` example: + +```toml +baseURL = "https://yourdomain.com/" +languageCode = "en-us" +title = "Your Site Title" +theme = "codex-obscura" +paginate = 8 +summaryLength = 20 + +[markup.highlight] + codeFences = false # disable Chroma — we use highlight.js instead + +[markup.goldmark.renderer] + unsafe = true # you can probably set this to false, try both. False works for me :) + +[markup.goldmark.extensions.passthrough] + enable = true + [markup.goldmark.extensions.passthrough.delimiters] + block = [["$$", "$$"]] + inline = [["$", "$"]] + +[params] + description = "Your site description shown in the hero card." + author = "Your Name" + tagline = "a brief italicized tagline" + sidebarQuote = "A quote you love." + sidebarQuoteAuthor = "The Author" + resumePDF = "/files/resume.pdf" + comentarioURL = "https://comments.yourdomain.com" + + [params.social] + github = "yourusername" + mastodon = "https://mastodon.social/@you" + email = "you@example.com" + +[[menu.main]] + name = "home" + url = "/" + weight = 1 +[[menu.main]] + name = "posts" + url = "/posts/" + weight = 2 +[[menu.main]] + name = "tags" + url = "/tags/" + weight = 3 +[[menu.main]] + name = "about" + url = "/about/" + weight = 4 +[[menu.main]] + name = "resume" + url = "/resume/" + weight = 5 +[[menu.main]] + name = "donate" + url = "/donate/" + weight = 6 + +[taxonomies] + tag = "tags" + category = "categories" +``` + +--- + +## Creating Content + +```bash +# New blog post +hugo new posts/my-entry.md + +# New about page +hugo new about.md +``` + +### Post front matter + +```yaml +--- +title: "My Entry" +date: 2024-10-31 +draft: false +description: "A brief description shown in meta tags." +summary: "Optional hand-written summary overriding auto-generation." +subtitle: "a quiet meditation on autumn leaves" +tags: ["autumn", "philosophy", "notes"] +author: "Archivist" +featured: true # pins to homepage +nocomments: true # disables comments on this post +--- +``` + +The `` divider can be placed anywhere in the body to manually control where the summary cuts off: + +```markdown +This sentence will appear as the summary. + + + +The rest of the post continues here. +``` + +--- + +## Special Pages + +### About + +Create `content/about.md` with any content. Hugo uses `_default/single.html`. + +### Resume + +Create `content/resume.md`: + +```yaml +--- +title: "Resume" +type: "resume" +layout: "single" +resumeName: "Your Name" +resumeTitle: "Your subtitle" +email: "you@example.com" +location: "somewhere cozy" +website: "https://yoursite.com" +github: "yourusername" +resumePDF: "/files/resume.pdf" +_build: + list: never +--- +``` + +Use `##` for sections, `###` for job/entry titles, `####` for subtitle/date lines. Place your PDF at `static/files/resume.pdf`. + +### Donate + +Create `content/donate.md`: + +```yaml +--- +title: "Support" +type: "donate" +layout: "single" +_build: + list: never +kofi: "yourusername" +liberapay: "yourusername" +github_sponsors: "yourusername" +bitcoin: "bc1qyouraddress" +ethereum: "0xyouraddress" +monero: "4youraddress" +litecoin: "ltc1youraddress" +--- +``` + +Crypto wallet cards include QR codes generated client-side in the site's amber color palette. Remove any platforms you don't use and they won't appear. + +--- + +## Syntax Highlighting + +The theme uses **highlight.js** instead of Hugo's built-in Chroma for better Common Lisp support as it's my preferred language. +This is already added to `layouts/partials/head.html`, and more can be added as you go as shown below: + +```html + + + + + + + + +``` + +### Custom code block shortcode + +Create `layouts/shortcodes/code.html` in your site root (this one you need to do!! I can't do it for you in advance): + +```html +
+ {{- with .Get "lang" }} +
+ {{ . }} + {{- with $.Get "title" }} + {{ . }} + {{- end }} +
+ {{- end }} +
+ {{ highlight (trim .Inner "\n") (.Get "lang" | default "text") "" }} +
+
+``` + +Use it in posts: + +``` +{{< code lang="python" title="fibonacci.py" >}} +def fib(n): + return n if n < 2 else fib(n-1) + fib(n-2) +{{< /code >}} +``` + +--- + +## LaTeX + +I added KaTeX to `layouts/partials/head.html` for math typesetting: + +```html + + + +``` + +Then write inline math with `$x^2$` and display math with `$$\int_0^\infty e^{-x}dx$$`. + +--- + +## Comments (Comentario) + +Install and run a [Comentario](https://gitlab.com/comentario/comentario) instance, then add to `hugo.toml`: + +```toml +[params] + comentarioURL = "https://comments.yourdomain.com" +``` + +Comments appear automatically on all posts. Disable per-post with `nocomments: true` in front matter. + +--- + +## Color Palette + +| Variable | Value | Use | +|--------------------|-----------|------------------------| +| `--amber` | `#d4a228` | Primary accent | +| `--orange` | `#c4601c` | Secondary accent | +| `--rust` | `#8a2e08` | Deep accent | +| `--forest-light` | `#4e7830` | Terminal prompt green | +| `--text-parchment` | `#e8d5a3` | Primary text | +| `--bg-primary` | `#130e07` | Main background | +| `--bg-secondary` | `#1c1509` | Card backgrounds | + +Override any variable by adding a `static/css/custom.css` and referencing it in `hugo.toml`: + +```toml +[params] + customCSS = ["css/custom.css"] +``` + +--- + +## Typography + +| Role | Font | Notes | +|----------|--------------|-----------------------------| +| Terminal | VT323 | Headings, nav, UI elements | +| Body | Josefin Sans | Paragraph text, sans-serif | +| Code | Courier Prime| Code blocks, monospace | + +All loaded from Google Fonts. + +--- + +## File Structure + +``` +codex-obscura/ +├── archetypes/ +│ └── default.md +├── exampleSite/ +│ ├── hugo.toml +│ └── content/ +│ ├── resume.md +│ └── donate.md +├── layouts/ +│ ├── index.html # Homepage +│ ├── _default/ +│ │ ├── baseof.html +│ │ ├── single.html # Single post +│ │ └── list.html # Archive / tag pages +│ ├── resume/ +│ │ └── single.html # Resume page +│ ├── donate/ +│ │ └── single.html # Donate page with QR codes +│ ├── tags/ +│ │ └── terms.html # Tag index +│ └── partials/ +│ ├── head.html +│ ├── header.html +│ ├── footer.html +│ ├── sidebar.html +│ └── comments.html # Comentario integration +├── static/ +│ ├── css/main.css +│ ├── js/main.js # Dust mote animation +│ └── favicon.svg +├── theme.toml +└── README.md +``` + +--- + +## License + +MIT — Please attribute my work to me! + +--- + +*"The library is infinite and the lights are always low."* diff --git a/archetypes/default.md b/archetypes/default.md new file mode 100644 index 0000000..40fa9ba --- /dev/null +++ b/archetypes/default.md @@ -0,0 +1,12 @@ +--- +title: "{{ replace .Name "-" " " | title }}" +date: {{ .Date }} +draft: true +description: "" +tags: [] +author: "" +featured: false +# lastmod: {{ .Date }} +--- + +An entry begins here. diff --git a/exampleSite/hugo.toml b/exampleSite/hugo.toml new file mode 100644 index 0000000..ce6be86 --- /dev/null +++ b/exampleSite/hugo.toml @@ -0,0 +1,54 @@ +baseURL = "https://example.com/" +languageCode = "en-us" +title = "Codex Obscura" +theme = "codex-obscura" +paginate = 8 + +[params] + description = "A wanderer in the archive. Keeper of fragments, collector of marginalia." + author = "Archivist" + tagline = "remnants & reflections" + + # Sidebar quote + sidebarQuote = "We are all just walking each other home." + sidebarQuoteAuthor = "Ram Dass" + + # Social links + [params.social] + github = "" + mastodon = "" + email = "" + + # Optional extra CSS files + # customCSS = ["css/custom.css"] + +[menu] + [[menu.main]] + name = "home" + url = "/" + weight = 1 + [[menu.main]] + name = "archive" + url = "/posts/" + weight = 2 + [[menu.main]] + name = "tags" + url = "/tags/" + weight = 3 + [[menu.main]] + name = "about" + url = "/about/" + weight = 4 + +[taxonomies] + tag = "tags" + category = "categories" + +[markup] + [markup.highlight] + style = "monokai" + lineNos = false + noClasses = true + guessSyntax = true + [markup.goldmark.renderer] + unsafe = true # allow raw HTML in markdown diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html new file mode 100644 index 0000000..d993c62 --- /dev/null +++ b/layouts/_default/baseof.html @@ -0,0 +1,17 @@ + + + + {{ partial "head.html" . }} + + + {{- partial "header.html" . -}} + +
+ {{- block "main" . }}{{- end }} +
+ + {{- partial "footer.html" . -}} + + + + diff --git a/layouts/_default/list.html b/layouts/_default/list.html new file mode 100644 index 0000000..19abcf4 --- /dev/null +++ b/layouts/_default/list.html @@ -0,0 +1,139 @@ +{{ define "main" }} +
+ +
+ + +
+

+ {{ if .IsHome }}ARCHIVE{{ else }}{{ .Title | upper }}{{ end }} +

+

+ ~/{{ .Section | default "posts" }} $ + ls -la • + {{ len .Pages }} entries +

+
+{{- $tags := .Site.Taxonomies.tags }} +{{- $currentTag := .Data.Term }} +{{- with $tags }} +
+ FILTER: + + all + + {{- range $name, $pages := $tags }} + + {{ $name }} + ({{ len $pages }}) + + {{- end }} +
+{{- end }} + + {{- if .Pages }} + + {{/* Group posts by year */}} + {{- range (.Pages.GroupByDate "2006") }} +
+ + +
+ + YEAR://{{ .Key }} + ({{ len .Pages }} entries) +
+ +
    + {{- range .Pages }} +
  • + +
    + $ cat + {{ .Date.Format "Jan 02" }} +
    + +

    + {{ .Title }} +

    + + {{- with .Params.subtitle }} +

    {{ . }}

    + {{- end }} + + {{- with .Summary }} +

    {{ . }}

    + {{- end }} + + {{- with .Params.tags }} +
    + {{- range . }} + {{ . }} + {{- end }} +
    + {{- end }} + +
  • + {{- end }} +
+ +
+ {{- end }} + + {{- else }} +
+

// QUERY RETURNED 0 RESULTS

+

+ The archive is silent. Return when manuscripts have been added. +

+
+ {{- end }} + + + {{- template "partials/pagination.html" . }} + +
+ + + {{ partial "sidebar.html" . }} + +
+{{ end }} + +{{- define "partials/pagination.html" }} +{{- $pager := .Paginate .Pages }} +{{- if gt $pager.TotalPages 1 }} + +{{- end }} +{{- end }} diff --git a/layouts/_default/single.html b/layouts/_default/single.html new file mode 100644 index 0000000..bdf875a --- /dev/null +++ b/layouts/_default/single.html @@ -0,0 +1,113 @@ +{{ define "main" }} +
+ + +
+ + +
+ + + + + +

{{ .Title }}

+ {{- with .Params.subtitle }} +

{{ . }}

+ {{- end }} + + + +
+ + +
+ {{ .Content }} +
+ + + + + {{ partial "comments.html" . }} +
+ + + {{ partial "sidebar.html" . }} + +
+{{ end }} diff --git a/layouts/donate/single.html b/layouts/donate/single.html new file mode 100644 index 0000000..1582acc --- /dev/null +++ b/layouts/donate/single.html @@ -0,0 +1,194 @@ +{{ define "main" }} +
+
+ + + + + + {{- with .Content }} +
{{ . }}
+ {{- end }} + + + + + + {{- $hasCrypto := or .Params.bitcoin .Params.ethereum .Params.monero .Params.litecoin }} + {{- if $hasCrypto }} + + {{- end }} + + + + +
+ + {{ partial "sidebar.html" . }} +
+{{ end }} diff --git a/layouts/index.html b/layouts/index.html new file mode 100644 index 0000000..fdb1342 --- /dev/null +++ b/layouts/index.html @@ -0,0 +1,133 @@ +{{ define "main" }} + + +
+
+ + + + +
+ +
+ SCHOLAR: + {{ .Site.Params.author | default "archivist" }} + STATUS: + reading +
+ + +
+ TEA: + + + + + +    + WHIMSY: + + + + + +
+ + +

+ {{- with .Site.Params.description -}} + {{ . }} + {{- else -}} + A wanderer in the archive. Keeper of fragments, collector of marginalia. + Pull up a chair. The kettle is on. + {{- end -}} +

+ + +
+ entries: + {{ len .Site.RegularPages }} +   + {{- with .Site.Taxonomies.tags }} + topics: + {{ len . }} + {{- end }} +
+
+ +
+ + +
+ + +
+
+ + {{- $pinned := where .Site.RegularPages "Params.featured" true }} + {{- $recent := first 8 (where .Site.RegularPages "Params.featured" "!=" true) }} + {{- $posts := $recent }} + {{- if $pinned }} + {{- $posts = union $pinned $recent }} + {{- end }} + + {{- if $posts }} +
    + {{- range $i, $post := first 2 $posts }} +
  • + +
    + ~/posts $ + {{ $post.Date.Format "2006-01-02" }} + {{- if $post.Params.featured }} + [FEATURED] + {{- end }} +
    + +

    + {{ $post.Title }} +

    + + {{- with $post.Summary }} +

    {{ . }}

    + {{- end }} + + {{- with $post.Params.tags }} +
    + {{- range . }} + {{ . }} + {{- end }} +
    + {{- end }} + +
  • + {{- end }} +
+ {{- else }} +
+

// No entries found. The archive awaits its first manuscript.

+
+ {{- end }} + + {{- if gt (len .Site.RegularPages) 2 }} + + {{- end }} + +
+ + {{ partial "sidebar.html" . }} + +
+ +{{ end }} diff --git a/layouts/partials/comments.html b/layouts/partials/comments.html new file mode 100644 index 0000000..5b5e598 --- /dev/null +++ b/layouts/partials/comments.html @@ -0,0 +1,13 @@ +{{- if and (not .Params.nocomments) (.Site.Params.comentarioURL) }} +
+
+ 💬 DISCUSSION +
+
+ + +
+{{- end }} diff --git a/layouts/partials/footer.html b/layouts/partials/footer.html new file mode 100644 index 0000000..5f8dab5 --- /dev/null +++ b/layouts/partials/footer.html @@ -0,0 +1,81 @@ +
+
+ + + + + + +
+
diff --git a/layouts/partials/head.html b/layouts/partials/head.html new file mode 100644 index 0000000..62679ac --- /dev/null +++ b/layouts/partials/head.html @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + +{{- with .Site.Params.description }} + +{{- end }} + +{{- with .Description }} + +{{- end }} + +{{- with .Site.Params.author }} + +{{- end }} + + + + + + +{{- with .Site.Params.ogImage }} + +{{- end }} + + + + + +{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} — {{ .Site.Title }}{{ end }} + + + + + +{{ range .AlternativeOutputFormats -}} + +{{ end -}} + + + + + + + + + + + + + +{{- with .Site.Params.customCSS }} +{{- range . }} + +{{- end }} +{{- end }} + + + diff --git a/layouts/partials/header.html b/layouts/partials/header.html new file mode 100644 index 0000000..80cd6b6 --- /dev/null +++ b/layouts/partials/header.html @@ -0,0 +1,51 @@ + diff --git a/layouts/partials/sidebar.html b/layouts/partials/sidebar.html new file mode 100644 index 0000000..65dde08 --- /dev/null +++ b/layouts/partials/sidebar.html @@ -0,0 +1,80 @@ + diff --git a/layouts/resume/single.html b/layouts/resume/single.html new file mode 100644 index 0000000..9c582cb --- /dev/null +++ b/layouts/resume/single.html @@ -0,0 +1,48 @@ +{{ define "main" }} +
+
+ + +
+
+
+ ~/resume $ + cat dossier.md +
+ + [ download .pdf ] + +
+ +

{{ .Params.resumeName | default .Site.Params.author | default "Your Name" }}

+ + {{- with .Params.resumeTitle }} +
{{ . }}
+ {{- end }} + +
+ {{- with .Params.email }} + mail: {{ . }} + {{- end }} + {{- with .Params.location }} + loc: {{ . }} + {{- end }} + {{- with .Params.website }} + web: {{ . }} + {{- end }} + {{- with .Params.github }} + git: {{ . }} + {{- end }} +
+
+ + +
+ {{ .Content }} +
+ +
+ + {{ partial "sidebar.html" . }} +
+{{ end }} diff --git a/layouts/tags/taxonomy.html b/layouts/tags/taxonomy.html new file mode 100644 index 0000000..19abcf4 --- /dev/null +++ b/layouts/tags/taxonomy.html @@ -0,0 +1,139 @@ +{{ define "main" }} +
+ +
+ + +
+

+ {{ if .IsHome }}ARCHIVE{{ else }}{{ .Title | upper }}{{ end }} +

+

+ ~/{{ .Section | default "posts" }} $ + ls -la • + {{ len .Pages }} entries +

+
+{{- $tags := .Site.Taxonomies.tags }} +{{- $currentTag := .Data.Term }} +{{- with $tags }} +
+ FILTER: + + all + + {{- range $name, $pages := $tags }} + + {{ $name }} + ({{ len $pages }}) + + {{- end }} +
+{{- end }} + + {{- if .Pages }} + + {{/* Group posts by year */}} + {{- range (.Pages.GroupByDate "2006") }} +
+ + +
+ + YEAR://{{ .Key }} + ({{ len .Pages }} entries) +
+ +
    + {{- range .Pages }} +
  • + +
    + $ cat + {{ .Date.Format "Jan 02" }} +
    + +

    + {{ .Title }} +

    + + {{- with .Params.subtitle }} +

    {{ . }}

    + {{- end }} + + {{- with .Summary }} +

    {{ . }}

    + {{- end }} + + {{- with .Params.tags }} +
    + {{- range . }} + {{ . }} + {{- end }} +
    + {{- end }} + +
  • + {{- end }} +
+ +
+ {{- end }} + + {{- else }} +
+

// QUERY RETURNED 0 RESULTS

+

+ The archive is silent. Return when manuscripts have been added. +

+
+ {{- end }} + + + {{- template "partials/pagination.html" . }} + +
+ + + {{ partial "sidebar.html" . }} + +
+{{ end }} + +{{- define "partials/pagination.html" }} +{{- $pager := .Paginate .Pages }} +{{- if gt $pager.TotalPages 1 }} + +{{- end }} +{{- end }} diff --git a/layouts/tags/terms.html b/layouts/tags/terms.html new file mode 100644 index 0000000..8434d5f --- /dev/null +++ b/layouts/tags/terms.html @@ -0,0 +1,30 @@ +{{ define "main" }} +
+
+ +
+

+ TOPIC INDEX +

+

+ ~/tags $ + ls • + {{ len .Data.Terms }} topics found +

+
+ + + +
+ {{ partial "sidebar.html" . }} +
+{{ end }} diff --git a/static/bitcoin_logo.png b/static/bitcoin_logo.png new file mode 100644 index 0000000..1e50ba7 Binary files /dev/null and b/static/bitcoin_logo.png differ diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..b04d572 --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,1491 @@ +/* ============================================================ + CODEX OBSCURA — A Dark Academia Pixel Theme + Fall Palette × Candlelit Library × Cozy Stationery + ============================================================ */ + +@import url('https://fonts.googleapis.com/css2?family=VT323&family=IM+Fell+English:ital@0;1&family=Courier+Prime:ital,wght@0,400;0,700;1,400&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Josefin+Sans:ital,wght@0,300;0,400;0,600;1,300;1,400&display=swap'); + +/* ── Variables ───────────────────────────────────────────── */ +:root { + /* Fall × Dark Academia Palette */ + --bg-void: #0b0804; + --bg-primary: #130e07; + --bg-secondary: #1c1509; + --bg-surface: #261c0e; + --bg-elevated: #312414; + --bg-hover: #3d2e1a; + + --text-parchment: #e8d5a3; + --text-warm: #c9b07a; + --text-muted: #8a7050; + --text-dim: #5a4830; + + --amber: #d4a228; + --amber-light: #f0c040; + --amber-dark: #a07818; + --orange: #c4601c; + --orange-light: #e07828; + --rust: #8a2e08; + --crimson: #7a1a1a; + --forest: #3a5c20; + --forest-light: #4e7830; + --umber: #5c3a18; + + --border-dim: #3a2a14; + --border-mid: #5a4020; + --border-bright: #8a6030; + --border-amber: #a07820; + + --glow-amber: rgba(212, 162, 40, 0.12); + --glow-text: rgba(232, 213, 163, 0.06); + + --font-terminal: 'VT323', 'Courier New', monospace; + --font-body: 'Josefin Sans', Georgia, 'Times New Roman', serif; + --font-mono: 'Courier Prime', 'Courier New', Courier, monospace; + + --px: 2px; + + --transition: 150ms ease; + --transition-slow: 400ms ease; +} + +/* ── Reset & Base ────────────────────────────────────────── */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 18px; + scroll-behavior: smooth; + background: var(--bg-void); +} + +::selection { + background: var(--amber-dark); + color: var(--bg-void); +} + +/* ── Body ────────────────────────────────────────────────── */ +body { + font-family: var(--font-body); + font-size: 1.05rem; + line-height: 1.75; + color: var(--text-parchment); + background: var(--bg-primary); + min-height: 100vh; + position: relative; + +} + +/* ── Layout Shell ────────────────────────────────────────── */ +.site-wrapper { + max-width: 1100px; + margin: 0 auto; + padding: 0 1.5rem; + overflow-x: hidden; + position: relative; + z-index: 1; +} + +.content-grid { + display: grid; + grid-template-columns: 1fr 280px; + gap: 2.5rem; + margin-top: 2rem; + align-items: start; +} + +@media (max-width: 768px) { + .content-grid { + grid-template-columns: 1fr; + } + .sidebar { order: -1; } +} + +/* ── Pixel Border Utility ────────────────────────────────── */ +.pixel-border { + border: var(--px) solid var(--border-mid); + box-shadow: + inset 0 0 0 var(--px) var(--border-dim), + var(--px) var(--px) 0 var(--border-bright), + calc(var(--px) * -1) calc(var(--px) * -1) 0 var(--border-dim); + position: relative; +} + +.pixel-border--amber { + border-color: var(--border-amber); + box-shadow: + inset 0 0 0 var(--px) rgba(212, 162, 40, 0.15), + var(--px) var(--px) 0 var(--amber), + calc(var(--px) * -1) calc(var(--px) * -1) 0 var(--amber-dark); +} + +/* ── Site Header ─────────────────────────────────────────── */ +.site-header { + padding: 2rem 0 1.5rem; + border-bottom: var(--px) solid var(--border-mid); + position: relative; + margin-bottom: 0; +} + +.site-header::after { + content: ''; + display: block; + height: var(--px); + background: linear-gradient(90deg, + transparent, + var(--amber-dark) 20%, + var(--amber) 50%, + var(--amber-dark) 80%, + transparent + ); + margin-top: var(--px); +} + +.header-inner { + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* Cozy pixel deco banner — replaces the heavy ASCII art */ +.site-deco { + font-family: var(--font-terminal); + font-size: 1.1rem; + color: var(--amber-dark); + letter-spacing: 0.3em; + opacity: 0.7; + user-select: none; +} + +/* Site title */ +.site-title { + display: flex; + align-items: baseline; + gap: 0.5rem; + text-decoration: none; +} + +.site-title__prompt { + font-family: var(--font-terminal); + font-size: 1.4rem; + color: var(--forest-light); +} + +.site-title__name { + font-family: var(--font-terminal); + font-size: clamp(1.8rem, 4vw, 2.8rem); + color: var(--text-parchment); + text-shadow: 2px 2px 0 var(--umber); + letter-spacing: 0.05em; + line-height: 1; + transition: color var(--transition); +} + +.site-title:hover .site-title__name { + color: var(--amber); +} + +.site-title__cursor { + font-family: var(--font-terminal); + font-size: 0.6rem; + color: var(--amber); + animation: blink 1.1s step-end infinite; +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +.site-tagline { + font-family: var(--font-body); + font-style: italic; + font-size: 0.9rem; + color: var(--text-muted); + letter-spacing: 0.08em; + margin-top: 0.2rem; +} + +/* ── Navigation ──────────────────────────────────────────── */ +.site-nav { + display: flex; + flex-wrap: wrap; + gap: 0; + font-family: var(--font-terminal); + font-size: 1.1rem; + margin-top: 0.5rem; + border-top: var(--px) solid var(--border-dim); + padding-top: 0.75rem; +} + +.nav-item a { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.3rem 1rem 0.3rem 0.5rem; + color: var(--text-warm); + text-decoration: none; + letter-spacing: 0.05em; + transition: color var(--transition), background var(--transition); +} + +.nav-item a::before { + content: './'; + color: var(--forest-light); + font-size: 0.9em; + opacity: 0.7; + transition: opacity var(--transition); +} + +.nav-item a:hover { + color: var(--amber-light); + background: var(--bg-elevated); +} + +.nav-item a:hover::before { + opacity: 1; + color: var(--amber); +} + +.nav-item--active a { + color: var(--amber); + background: var(--bg-elevated); +} + +.nav-item--active a::before { + color: var(--orange); +} + +/* ── Main Content ────────────────────────────────────────── */ +main { min-width: 0; } + +/* ── Post Card ───────────────────────────────────────────── */ +.post-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.post-card { + background: var(--bg-secondary); + padding: 1.5rem; + position: relative; + transition: background var(--transition), border-color var(--transition); +} + +.post-card.pixel-border:hover { + background: var(--bg-surface); + border-color: var(--border-amber); + box-shadow: + inset 0 0 0 var(--px) rgba(212, 162, 40, 0.1), + var(--px) var(--px) 0 var(--amber-dark), + calc(var(--px) * -1) calc(var(--px) * -1) 0 var(--umber); +} + +/* small pixel icon top-right */ +.post-card::before { + content: attr(data-index); + font-family: var(--font-terminal); + font-size: 0.75rem; + color: var(--text-dim); + position: absolute; + top: 0.5rem; + right: 0.75rem; + letter-spacing: 0.1em; +} + +.post-card__meta { + font-family: var(--font-terminal); + font-size: 0.9rem; + color: var(--forest-light); + margin-bottom: 0.4rem; + display: flex; + align-items: center; + gap: 0.5rem; + letter-spacing: 0.05em; +} + +.post-card__meta .prompt { color: var(--text-dim); } +.post-card__date { color: var(--text-muted); } + +.post-card__title { + font-family: var(--font-terminal); + font-size: clamp(1.3rem, 2.5vw, 1.7rem); + line-height: 1.2; + margin-bottom: 0.6rem; + letter-spacing: 0.03em; +} + +.post-card__title a { + color: var(--text-parchment); + text-decoration: none; + transition: color var(--transition); + text-shadow: 1px 1px 0 var(--umber); +} + +.post-card__title a:hover { color: var(--amber); } + +.post-card__summary { + font-size: 0.92rem; + color: var(--text-warm); + line-height: 1.7; + font-style: italic; + border-left: 2px solid var(--border-mid); + padding-left: 0.8rem; + margin-bottom: 0.8rem; +} + +.post-card__tags { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} + +.tag { + font-family: var(--font-terminal); + font-size: 0.78rem; + padding: 0.1rem 0.5rem; + background: var(--bg-elevated); + color: var(--text-muted); + border: var(--px) solid var(--border-dim); + text-decoration: none; + letter-spacing: 0.08em; + transition: all var(--transition); +} + +.tag:hover { + background: var(--bg-hover); + color: var(--amber); + border-color: var(--amber-dark); +} + +.tag::before { + content: '#'; + color: var(--amber-dark); + opacity: 0.7; +} + +/* ── Single Post ─────────────────────────────────────────── */ +.post-header { + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: var(--px) solid var(--border-mid); + position: relative; +} + +.post-header::after { + content: ''; + display: block; + height: var(--px); + background: linear-gradient(90deg, + var(--amber-dark), + var(--orange) 40%, + var(--rust) 70%, + transparent + ); + margin-top: var(--px); +} + +.post-breadcrumb { + font-family: var(--font-terminal); + font-size: 0.85rem; + color: var(--text-dim); + margin-bottom: 0.8rem; + letter-spacing: 0.05em; +} + +.post-breadcrumb a { + color: var(--forest-light); + text-decoration: none; +} + +.post-breadcrumb a:hover { color: var(--amber); } +.post-breadcrumb span { color: var(--border-bright); margin: 0 0.3rem; } + +.post-title { + font-family: var(--font-terminal); + font-size: clamp(1.8rem, 4vw, 3rem); + color: var(--text-parchment); + line-height: 1.15; + letter-spacing: 0.04em; + text-shadow: 2px 2px 0 var(--umber); + margin-bottom: 0.75rem; +} + +.post-title::before { + content: '> '; + color: var(--orange); + font-size: 0.85em; +} + +.post-meta { + font-family: var(--font-terminal); + font-size: 0.9rem; + color: var(--text-muted); + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.3rem 1rem; + letter-spacing: 0.05em; +} + +.post-meta__item { display: flex; align-items: center; gap: 0.3rem; } +.post-meta__label { color: var(--forest-light); } +.post-meta__value { color: var(--amber); } +.post-reading-time { color: var(--text-dim); } + +/* ── Article Body ────────────────────────────────────────── */ +.post-body { max-width: 68ch; } + +.post-body h1, +.post-body h2, +.post-body h3, +.post-body h4, +.post-body h5, +.post-body h6 { + font-family: var(--font-terminal); + color: var(--amber-light); + letter-spacing: 0.05em; + margin: 2rem 0 0.75rem; + line-height: 1.2; +} + +.post-body h2 { font-size: 1.7rem; } +.post-body h3 { font-size: 1.4rem; } +.post-body h4 { font-size: 1.2rem; } + +# .post-body h2::before { content: '## '; color: var(--orange); opacity: 0.7; } +# .post-body h3::before { content: '### '; color: var(--orange); opacity: 0.6; } +# .post-body h4::before { content: '#### '; color: var(--orange); opacity: 0.5; } + +.post-body p { margin-bottom: 1.3rem; color: var(--text-warm); } + +.post-body a { + color: var(--amber); + text-decoration: none; + border-bottom: var(--px) solid var(--amber-dark); + padding-bottom: 1px; + transition: color var(--transition), border-color var(--transition); +} + +.post-body a:hover { + color: var(--amber-light); + border-color: var(--amber-light); +} + +.post-body strong { color: var(--text-parchment); font-weight: 700; } +.post-body em { color: var(--text-parchment); font-style: italic; } + +.post-body blockquote { + margin: 1.5rem 0; + padding: 1rem 1.25rem; + background: var(--bg-secondary); + border-left: 4px solid var(--amber-dark); + border-right: var(--px) solid var(--border-dim); + position: relative; + font-style: italic; + color: var(--text-warm); +} + +.post-body blockquote::before { + content: '"'; + font-family: var(--font-terminal); + font-size: 3rem; + color: var(--amber-dark); + position: absolute; + top: -0.5rem; + left: 0.75rem; + line-height: 1; + opacity: 0.5; +} + +.post-body blockquote p { margin: 0; padding-left: 1rem; } + +.post-body code { + font-family: var(--font-mono); + font-size: 0.88em; + background: var(--bg-secondary); + color: var(--orange-light); + padding: 0.15em 0.45em; + border: var(--px) solid var(--border-dim); +} + +.post-body pre { + background: var(--bg-void); + border: var(--px) solid var(--border-mid); + box-shadow: + inset 0 0 0 var(--px) var(--border-dim), + var(--px) var(--px) 0 var(--forest); + padding: 1.25rem; + overflow-x: auto; + margin: 1.5rem 0; + position: relative; +} + +.post-body pre::before { + content: '~/codex $ '; + display: block; + font-family: var(--font-terminal); + font-size: 0.8rem; + color: var(--forest-light); + margin-bottom: 0.5rem; + letter-spacing: 0.05em; +} + +.post-body pre code { + background: none; + border: none; + padding: 0; + color: var(--text-parchment); + font-size: 0.9rem; + line-height: 1.6; +} + +.post-body hr { + border: none; + margin: 2rem 0; + height: 1px; + background: repeating-linear-gradient( + 90deg, + var(--border-mid) 0px, + var(--border-mid) 4px, + transparent 4px, + transparent 8px + ); + position: relative; +} + +.post-body hr::after { + content: '✦ ✦ ✦'; + font-size: 0.7rem; + color: var(--amber-dark); + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--bg-primary); + padding: 0 0.75rem; + letter-spacing: 0.4em; +} + +.post-body ul, +.post-body ol { + margin: 0 0 1.3rem 1.2rem; + color: var(--text-warm); +} + +.post-body li { margin-bottom: 0.35rem; position: relative; } +.post-body ul > li { list-style: none; } + +.post-body ul > li::before { + content: '›'; + font-family: var(--font-terminal); + color: var(--orange); + position: absolute; + left: -1.1rem; + top: 0; +} + +.post-body table { + width: 100%; + border-collapse: collapse; + font-family: var(--font-mono); + font-size: 0.88rem; + margin: 1.5rem 0; +} + +.post-body th { + background: var(--bg-elevated); + color: var(--amber); + font-family: var(--font-terminal); + letter-spacing: 0.08em; + padding: 0.5rem 0.75rem; + border: var(--px) solid var(--border-mid); + text-align: left; +} + +.post-body td { + padding: 0.45rem 0.75rem; + border: var(--px) solid var(--border-dim); + color: var(--text-warm); + border-bottom-color: var(--border-mid); +} + +.post-body tr:nth-child(even) td { background: var(--bg-secondary); } + +.post-body img { + max-width: 100%; + height: auto; + display: block; + margin: 1.5rem auto; + border: var(--px) solid var(--border-mid); + box-shadow: var(--px) var(--px) 0 var(--umber); + image-rendering: auto; +} + +.post-body figcaption { + font-family: var(--font-terminal); + font-size: 0.8rem; + color: var(--text-dim); + text-align: center; + margin-top: 0.4rem; + letter-spacing: 0.05em; +} + +/* ── Post Footer ─────────────────────────────────────────── */ +.post-footer { + margin-top: 3rem; + padding-top: 1.5rem; + border-top: var(--px) solid var(--border-mid); +} + +.post-tags-section { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + margin-bottom: 1.5rem; + font-family: var(--font-terminal); + font-size: 0.85rem; + color: var(--text-dim); +} + +.post-tags-section__label { color: var(--forest-light); letter-spacing: 0.05em; } + +.post-nav { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.post-nav__item { + background: var(--bg-secondary); + padding: 1rem; + text-decoration: none; + transition: background var(--transition); + border: var(--px) solid var(--border-dim); +} + +.post-nav__item:hover { + background: var(--bg-elevated); + border-color: var(--border-bright); +} + +.post-nav__item--next { text-align: right; } + +.post-nav__label { + font-family: var(--font-terminal); + font-size: 0.75rem; + color: var(--forest-light); + letter-spacing: 0.1em; + display: block; + margin-bottom: 0.25rem; +} + +.post-nav__title { + font-family: var(--font-terminal); + font-size: 0.95rem; + color: var(--text-parchment); + transition: color var(--transition); +} + +.post-nav__item:hover .post-nav__title { color: var(--amber); } + +/* ── Sidebar ─────────────────────────────────────────────── */ +.sidebar { + font-family: var(--font-terminal); + position: sticky; + top: 1.5rem; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.sidebar-widget { + background: var(--bg-secondary); + padding: 1rem; +} + +.sidebar-widget__header { + font-size: 0.85rem; + color: var(--amber); + letter-spacing: 0.12em; + text-transform: uppercase; + padding-bottom: 0.6rem; + margin-bottom: 0.75rem; + border-bottom: var(--px) solid var(--border-dim); + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* pixel icon before widget title */ +.sidebar-widget__header .widget-icon { + font-size: 1rem; + line-height: 1; +} + +.sidebar-posts { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.sidebar-post { + border-left: 2px solid var(--border-dim); + padding-left: 0.6rem; + transition: border-color var(--transition); +} + +.sidebar-post:hover { border-left-color: var(--amber-dark); } + +.sidebar-post a { + font-size: 0.85rem; + color: var(--text-muted); + text-decoration: none; + display: block; + transition: color var(--transition); + line-height: 1.3; +} + +.sidebar-post a:hover { color: var(--amber); } + +.sidebar-post__date { + font-size: 0.7rem; + color: var(--text-dim); + display: block; + margin-top: 0.1rem; +} + +.sidebar-tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.sys-info { + font-size: 0.8rem; + color: var(--text-dim); + line-height: 1.9; +} + +.sys-info__row { display: flex; gap: 0.5rem; } +.sys-info__key { color: var(--forest-light); min-width: 80px; } +.sys-info__val { color: var(--text-muted); } + +/* ── Homepage Hero ───────────────────────────────────────── */ +.hero { + padding: 2.5rem 0 2rem; + border-bottom: var(--px) solid var(--border-dim); + margin-bottom: 2rem; +} + +/* Cozy intro card — like the RPG status box in the reference */ +.hero__card { + background: var(--bg-secondary); + padding: 1.5rem; + margin-bottom: 1.5rem; + display: grid; + grid-template-columns: auto 1fr; + gap: 2.5rem; + align-items: start; +} + +.hero__pixel-art { + font-family: var(--font-terminal); + font-size: 0.72rem; + line-height: 1.25; + color: var(--amber); + white-space: pre; + user-select: none; + flex-shrink: 0; +} + +.hero__info { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.hero__name-bar { + font-family: var(--font-terminal); + font-size: 1.1rem; + color: var(--text-parchment); + background: var(--bg-elevated); + padding: 0.3rem 0.75rem; + display: flex; + align-items: center; + justify-content: space-between; + border: var(--px) solid var(--border-dim); +} + +.hero__name-bar .label { color: var(--text-dim); font-size: 0.85em; } +.hero__name-bar .value { color: var(--amber); } + +.hero__desc { + font-family: var(--font-mono); + font-size: 0.92rem; + color: var(--text-warm); + line-height: 1.7; +} + +/* Cozy status pip row */ +.hero__status { + display: flex; + align-items: center; + gap: 0.4rem; + font-family: var(--font-terminal); + font-size: 0.8rem; + color: var(--text-dim); +} + +.pip { + display: inline-block; + width: 10px; + height: 10px; + background: var(--amber); + border: var(--px) solid var(--amber-dark); + image-rendering: pixelated; +} + +.pip--dim { background: var(--bg-elevated); border-color: var(--border-mid); } + +@media (max-width: 600px) { + .hero__card { grid-template-columns: 1fr; } + .hero__pixel-art { display: none; } +} + +.hero__cta { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.btn { + font-family: var(--font-terminal); + font-size: 1rem; + letter-spacing: 0.08em; + padding: 0.55rem 1.4rem; + text-decoration: none; + border: var(--px) solid; + transition: all var(--transition); + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 0.4rem; +} + +.btn--primary { + background: var(--amber-dark); + color: var(--bg-void); + border-color: var(--amber); + box-shadow: var(--px) var(--px) 0 var(--umber); +} + +.btn--primary:hover { + background: var(--amber); + box-shadow: var(--px) var(--px) 0 var(--amber-dark); + transform: translate(-1px, -1px); +} + +.btn--secondary { + background: transparent; + color: var(--text-warm); + border-color: var(--border-mid); +} + +.btn--secondary:hover { + color: var(--amber); + border-color: var(--amber-dark); + background: var(--bg-elevated); +} + +/* ── Pagination ──────────────────────────────────────────── */ +.pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; + margin: 2.5rem 0; + font-family: var(--font-terminal); + font-size: 0.9rem; +} + +.pagination a, +.pagination span { + padding: 0.35rem 0.75rem; + border: var(--px) solid var(--border-dim); + text-decoration: none; + transition: all var(--transition); +} + +.pagination a { color: var(--text-muted); } + +.pagination a:hover { + color: var(--amber); + border-color: var(--amber-dark); + background: var(--bg-elevated); +} + +.pagination .current { + background: var(--bg-elevated); + color: var(--amber); + border-color: var(--border-bright); +} + +/* ── Taxonomy ────────────────────────────────────────────── */ +.taxonomy-header { + font-family: var(--font-terminal); + font-size: 0.85rem; + color: var(--text-dim); + letter-spacing: 0.08em; + margin-bottom: 1.5rem; +} + +.taxonomy-list { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + list-style: none; +} + +.taxonomy-list__item a { + font-family: var(--font-terminal); + font-size: 1rem; + padding: 0.35rem 0.8rem; + background: var(--bg-secondary); + color: var(--text-muted); + text-decoration: none; + border: var(--px) solid var(--border-dim); + display: inline-flex; + align-items: center; + gap: 0.4rem; + transition: all var(--transition); + letter-spacing: 0.05em; +} + +.taxonomy-list__item a:hover { + color: var(--amber); + border-color: var(--amber-dark); + background: var(--bg-elevated); +} + +.taxonomy-list__item .count { color: var(--text-dim); font-size: 0.8em; } + +/* ── Site Footer ─────────────────────────────────────────── */ +.site-footer { + margin-top: 4rem; + padding: 0; + border-top: var(--px) solid var(--border-mid); + position: relative; +} + +.site-footer::before { + content: ''; + display: block; + height: var(--px); + background: linear-gradient(90deg, + transparent, + var(--amber-dark) 20%, + var(--amber) 50%, + var(--amber-dark) 80%, + transparent + ); +} + +.footer-inner { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 1.5rem; + padding: 1.5rem 0; + border-bottom: var(--px) solid var(--border-dim); +} + +@media (max-width: 600px) { .footer-inner { grid-template-columns: 1fr; } } + +.footer-col__title { + font-family: var(--font-terminal); + font-size: 0.8rem; + color: var(--forest-light); + letter-spacing: 0.12em; + margin-bottom: 0.6rem; +} + +.footer-links { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.footer-links a { + font-family: var(--font-terminal); + font-size: 0.85rem; + color: var(--text-dim); + text-decoration: none; + transition: color var(--transition); +} + +.footer-links a:hover { color: var(--amber); } + +.footer-links a::before { + content: '> '; + color: var(--border-bright); + opacity: 0; + transition: opacity var(--transition); +} + +.footer-links a:hover::before { opacity: 1; } + +.footer-status { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 0; + font-family: var(--font-terminal); + font-size: 0.75rem; + color: var(--text-dim); + letter-spacing: 0.08em; + flex-wrap: wrap; + gap: 0.5rem; +} + +.status-left, +.status-right { + display: flex; + align-items: center; + gap: 1rem; +} + +.status-indicator { + display: inline-flex; + align-items: center; + gap: 0.3rem; +} + +.status-dot { + width: 6px; + height: 6px; + background: var(--forest-light); + display: inline-block; + animation: pulse 3s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.3; } +} + +/* ── Utility ─────────────────────────────────────────────── */ +.sr-only { + position: absolute; + width: 1px; height: 1px; + padding: 0; margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + white-space: nowrap; + border: 0; +} + +/* ── Scrollbar ───────────────────────────────────────────── */ +::-webkit-scrollbar { width: 8px; height: 8px; } +::-webkit-scrollbar-track { background: var(--bg-void); } +::-webkit-scrollbar-thumb { + background: var(--umber); + border: var(--px) solid var(--bg-secondary); +} +::-webkit-scrollbar-thumb:hover { background: var(--amber-dark); } + +/* ── Print ───────────────────────────────────────────────── */ +@media print { + body::before, body::after, .site-nav, .sidebar, .post-nav { display: none; } + body { background: #fff; color: #000; } +} + +/* ── Resume Page ─────────────────────────────────────────── */ +.resume-header { + background: var(--bg-secondary); + padding: 1.5rem; + margin-bottom: 2rem; +} + +.resume-header__top { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + font-family: var(--font-terminal); + font-size: 0.85rem; +} + +.resume-name { + font-family: var(--font-terminal); + font-size: clamp(2rem, 4vw, 3rem); + color: var(--text-parchment); + letter-spacing: 0.06em; + text-shadow: 2px 2px 0 var(--umber); + line-height: 1.1; + margin-bottom: 0.3rem; +} + +.resume-title { + font-family: var(--font-body); + font-style: italic; + font-size: 1rem; + color: var(--amber); + margin-bottom: 0.75rem; + letter-spacing: 0.05em; +} + +.resume-contacts { + display: flex; + flex-wrap: wrap; + gap: 0.4rem 1.5rem; + font-family: var(--font-terminal); + font-size: 0.82rem; +} + +.resume-contact__label { + color: var(--forest-light); +} + +.resume-contact a { + color: var(--text-muted); + text-decoration: none; + border-bottom: var(--px) solid var(--border-dim); + transition: color var(--transition), border-color var(--transition); +} + +.resume-contact a:hover { + color: var(--amber); + border-color: var(--amber-dark); +} + +/* Resume sections inherit post-body styles but with some overrides */ +.resume-body { + max-width: 72ch; +} + +.resume-body h2 { + font-family: var(--font-terminal); + font-size: 1.3rem; + color: var(--amber-light); + letter-spacing: 0.1em; + text-transform: uppercase; + margin: 2rem 0 1rem; + padding-bottom: 0.4rem; + border-bottom: var(--px) solid var(--border-mid); + position: relative; +} + +.resume-body h2::before { + content: '▸ '; + color: var(--orange); + font-size: 0.9em; +} + +/* Job/entry block */ +.resume-body h3 { + font-family: var(--font-terminal); + font-size: 1.05rem; + color: var(--text-parchment); + letter-spacing: 0.04em; + margin: 1.2rem 0 0.1rem; +} + +.resume-body h3::before { content: none; } + +.resume-body h4 { + font-family: var(--font-terminal); + font-size: 0.85rem; + color: var(--forest-light); + letter-spacing: 0.08em; + margin: 0 0 0.5rem; + font-weight: normal; +} + +.resume-body h4::before { content: none; } + +.resume-body p { + color: var(--text-warm); + font-size: 0.95rem; + margin-bottom: 0.75rem; +} + +.resume-body ul { + margin: 0.25rem 0 0.75rem 1rem; + color: var(--text-warm); +} + +.resume-body ul > li { + list-style: none; + font-size: 0.92rem; + margin-bottom: 0.25rem; + position: relative; +} + +.resume-body ul > li::before { + content: '›'; + font-family: var(--font-terminal); + color: var(--orange); + position: absolute; + left: -1rem; +} + +/* Skills tag row — wrap a group of words in a

inside a section */ +.resume-body .skills-row { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin-bottom: 0.75rem; +} + +/* Horizontal rule between sections */ +.resume-body hr { + border: none; + margin: 1.5rem 0; + height: var(--px); + background: repeating-linear-gradient( + 90deg, + var(--border-dim) 0px, + var(--border-dim) 4px, + transparent 4px, + transparent 8px + ); +} + +@media print { + .resume-header__top .btn, + .sidebar, + .site-header, + .site-footer, + body::before, + body::after { display: none !important; } + + .content-grid { grid-template-columns: 1fr !important; } + .resume-name { font-size: 2rem; } + body { background: #fff; color: #000; } + .resume-header { border: 1px solid #ccc; } +} + +/* ── Donate Page ─────────────────────────────────────────── */ +.donate-header { + background: var(--bg-secondary); + padding: 1.5rem; + margin-bottom: 2rem; +} + +.donate-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1rem; + margin-bottom: 2.5rem; +} + +.donate-card { + background: var(--bg-secondary); + padding: 1.25rem; + text-decoration: none; + display: flex; + flex-direction: column; + gap: 0.3rem; + transition: background var(--transition), border-color var(--transition); +} + +.donate-card:hover { + background: var(--bg-elevated); + border-color: var(--border-amber); +} + +.donate-card__icon { + font-size: 1.8rem; + line-height: 1; + margin-bottom: 0.25rem; +} + +.donate-card__name { + font-family: var(--font-terminal); + font-size: 1.1rem; + color: var(--amber); + letter-spacing: 0.06em; +} + +.donate-card__handle { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--forest-light); +} + +.donate-card__desc { + font-family: var(--font-body); + font-style: italic; + font-size: 0.85rem; + color: var(--text-muted); + margin-top: 0.25rem; +} + +.donate-section { + margin-top: 1rem; +} + +.donate-section__title { + font-family: var(--font-terminal); + font-size: 1.1rem; + color: var(--amber-light); + letter-spacing: 0.1em; + text-transform: uppercase; + padding-bottom: 0.4rem; + margin-bottom: 1rem; + border-bottom: var(--px) solid var(--border-mid); +} + +.donate-section__title::before { + content: '▸ '; + color: var(--orange); +} + +.wallet-card { + background: var(--bg-secondary); + padding: 1rem 1.25rem; + margin-bottom: 0.75rem; +} + +.wallet-card__header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.wallet-card__icon { + font-family: var(--font-terminal); + font-size: 1.2rem; + color: var(--amber); + width: 1.5rem; + text-align: center; +} + +.wallet-card__name { + font-family: var(--font-terminal); + font-size: 1rem; + color: var(--text-parchment); + letter-spacing: 0.05em; +} + +.wallet-card__ticker { + font-family: var(--font-terminal); + font-size: 0.75rem; + color: var(--text-dim); + margin-left: auto; + letter-spacing: 0.1em; +} + +.wallet-card__address { + display: flex; + align-items: center; + gap: 0.75rem; + background: var(--bg-void); + padding: 0.5rem 0.75rem; + border: var(--px) solid var(--border-dim); + flex-wrap: wrap; +} + +.wallet-card__addr-text { + font-family: var(--font-mono); + font-size: 0.78rem; + color: var(--orange-light); + word-break: break-all; + flex: 1; +} + +.wallet-card__copy { + font-family: var(--font-terminal); + font-size: 0.82rem; + color: var(--text-dim); + background: none; + border: none; + cursor: pointer; + padding: 0; + transition: color var(--transition); + white-space: nowrap; + flex-shrink: 0; +} + +.wallet-card__copy:hover { + color: var(--amber); +} +/* ── Wallet QR codes ─────────────────────────────────────── */ +.wallet-card__body { + display: flex; + align-items: flex-start; + gap: 1rem; + flex-wrap: wrap; +} + +.wallet-card__address { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 0.75rem; + background: var(--bg-void); + padding: 0.5rem 0.75rem; + border: var(--px) solid var(--border-dim); + flex-wrap: wrap; +} + +.wallet-qr { + width: 160px; + height: 160px; + flex-shrink: 0; + border: var(--px) solid var(--border-mid); + box-shadow: var(--px) var(--px) 0 var(--umber); + image-rendering: pixelated; +} + +.post-subtitle { + font-family: var(--font-body); + font-style: italic; + color: var(--text-muted); + font-size: 0.9rem; + margin-top: -0.3rem; + margin-bottom: 0.6rem; + letter-spacing: 0.02em; +} + + +.code-block__header { + font-family: var(--font-terminal); + font-size: 0.8rem; + background: var(--bg-elevated); + border: var(--px) solid var(--border-mid); + border-bottom: none; + padding: 0.3rem 1rem; + display: flex; + align-items: center; + justify-content: space-between; + letter-spacing: 0.08em; +} + +.code-block__lang { + color: var(--forest-light); +} + +.code-block__title { + color: var(--text-dim); + font-style: normal; +} + +.code-block__body pre { + margin: 0; +} + +.code-block__body pre::before { + content: none; +} diff --git a/static/ethereum_logo.png b/static/ethereum_logo.png new file mode 100644 index 0000000..765d2d8 Binary files /dev/null and b/static/ethereum_logo.png differ diff --git a/static/favicon.svg b/static/favicon.svg new file mode 100644 index 0000000..20b37f0 --- /dev/null +++ b/static/favicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..2b909a5 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,152 @@ +/* ============================================================ + CODEX OBSCURA — main.js + Dust motes drifting in candlelight. + ============================================================ */ + +(function () { + 'use strict'; + + document.documentElement.classList.remove('no-js'); + document.documentElement.classList.add('js'); + + /* ── Uptime Counter ──────────────────────────────────────── */ + const uptimeEl = document.getElementById('js-uptime'); + if (uptimeEl) { + const startTime = Date.now(); + function updateUptime() { + const elapsed = Math.floor((Date.now() - startTime) / 1000); + const h = String(Math.floor(elapsed / 3600)).padStart(2, '0'); + const m = String(Math.floor((elapsed % 3600) / 60)).padStart(2, '0'); + const s = String(elapsed % 60).padStart(2, '0'); + uptimeEl.textContent = h + ':' + m + ':' + s; + } + setInterval(updateUptime, 1000); + updateUptime(); + } + + /* ── Keyboard shortcuts ──────────────────────────────────── */ + document.addEventListener('keydown', function (e) { + if (e.key === '/' && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') { + const search = document.getElementById('search-input'); + if (search) { e.preventDefault(); search.focus(); } + } + if (e.key === 'Escape' && document.activeElement) { + document.activeElement.blur(); + } + }); + + /* ── Dust Motes ──────────────────────────────────────────── */ + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + + const canvas = document.createElement('canvas'); + canvas.style.cssText = [ + 'position:absolute', 'top:0', 'left:0', + 'width:100%', 'height:100%', + 'pointer-events:none', 'z-index:2' + ].join(';'); + + document.body.appendChild(canvas); + const ctx = canvas.getContext('2d'); + + + let W = document.body.scrollWidth; + let H = document.body.scrollHeight; + canvas.width = W; + canvas.height = H; + + window.addEventListener('resize', function () { + W = canvas.width = document.body.scrollWidth; + H = canvas.height = document.body.scrollHeight; + }); + + // Warm amber/parchment dust colours + const COLORS = [ + '212, 162, 40', // amber + '201, 176, 122', // parchment warm + '196, 96, 28', // orange + '138, 96, 48', // umber + '232, 213, 163', // parchment light + ' 78, 120, 48', // forest + ]; + + const MOTE_COUNT = 500; + + function rand(min, max) { return min + Math.random() * (max - min); } + + function makeMote(scatterY) { + // Each mote has its own independent fade cycle driven by a sine wave + // so they breathe in and out at different rates rather than all + // appearing and disappearing together. + const peakAlpha = rand(0.06, 0.22); + return { + x: rand(0, W), + y: scatterY !== undefined ? scatterY : rand(0, H), + // very slow drift — tiny random velocity + vx: rand(-0.12, 0.12), + vy: rand(-0.08, 0.06), // slight upward bias like warm air + // size: 1–2.5px radius, soft and small + r: rand(1.2, 2.4), + color: COLORS[Math.floor(Math.random() * COLORS.length)], + peakAlpha, + // sine-wave phase and speed for opacity breathing + phase: rand(0, Math.PI * 2), + phaseSpeed: rand(0.003, 0.009), // full breath every ~12–35 seconds + // gentle Brownian wobble + wobbleX: rand(0, Math.PI * 2), + wobbleY: rand(0, Math.PI * 2), + wobbleSpeedX: rand(0.002, 0.007), + wobbleSpeedY: rand(0.002, 0.006), + wobbleAmp: rand(0.04, 0.18), // pixels of wobble per frame + }; + } + + // Seed motes scattered across the whole viewport + let motes = Array.from({ length: MOTE_COUNT }, function () { return makeMote(rand(0, H)); }); + + function animate() { + ctx.clearRect(0, 0, W, H); + + const now = performance.now() * 0.001; // seconds, for wobble + + for (let i = 0; i < motes.length; i++) { + const m = motes[i]; + + // Advance fade phase + m.phase += m.phaseSpeed; + + // Opacity: sine wave between 0 and peakAlpha, always non-negative + const alpha = m.peakAlpha * (0.5 + 0.5 * Math.sin(m.phase)); + + // Drift position + m.x += m.vx + Math.sin(m.wobbleX) * m.wobbleAmp; + m.y += m.vy + Math.cos(m.wobbleY) * m.wobbleAmp * 0.6; + + // Advance wobble phases independently of main phase + m.wobbleX += m.wobbleSpeedX; + m.wobbleY += m.wobbleSpeedY; + + // Wrap around edges with a small buffer so motes never pop in visibly + const buf = 4; + if (m.x < -buf) m.x = W + buf; + if (m.x > W + buf) m.x = -buf; + if (m.y < -buf) m.y = H + buf; + if (m.y > H + buf) m.y = -buf; + + // Draw as a soft radial-gradient circle (blurred disc, not a hard pixel) + const grad = ctx.createRadialGradient(m.x, m.y, 0, m.x, m.y, m.r * 2.5); + grad.addColorStop(0, 'rgba(' + m.color + ', ' + alpha + ')'); + grad.addColorStop(0.5, 'rgba(' + m.color + ', ' + (alpha * 0.4) + ')'); + grad.addColorStop(1, 'rgba(' + m.color + ', 0)'); + + ctx.beginPath(); + ctx.arc(m.x, m.y, m.r * 2.5, 0, Math.PI * 2); + ctx.fillStyle = grad; + ctx.fill(); + } + + requestAnimationFrame(animate); + } + + animate(); + +})(); diff --git a/static/kofi.webp b/static/kofi.webp new file mode 100644 index 0000000..7aea4a9 Binary files /dev/null and b/static/kofi.webp differ diff --git a/static/liberapay.svg b/static/liberapay.svg new file mode 100644 index 0000000..ec2a0c1 --- /dev/null +++ b/static/liberapay.svg @@ -0,0 +1 @@ + diff --git a/static/litecoin.png b/static/litecoin.png new file mode 100644 index 0000000..318967c Binary files /dev/null and b/static/litecoin.png differ diff --git a/static/monero_logo.png b/static/monero_logo.png new file mode 100644 index 0000000..9cb993d Binary files /dev/null and b/static/monero_logo.png differ diff --git a/static/profile_pic.png b/static/profile_pic.png new file mode 100644 index 0000000..f9f729c Binary files /dev/null and b/static/profile_pic.png differ diff --git a/theme.toml b/theme.toml new file mode 100644 index 0000000..fa26333 --- /dev/null +++ b/theme.toml @@ -0,0 +1,16 @@ +name = "Codex Obscura" +license = "MIT" +licenselink = "https://opensource.org/licenses/MIT" +description = "A dark academia pixel terminal Hugo theme with fall color palette. Blends the warmth of candlelit manuscripts with the glow of a CRT screen." +homepage = "" +min_version = "0.80.0" +tags = ["dark", "academia", "terminal", "pixel", "blog", "portfolio"] + +[author] + name = "Codex Obscura" + homepage = "" + +[features] + [features.outputFormats] + [features.outputFormats.disableAliases] + disabled = false