A => .gitignore +1 -0
A => CLAUDE.md +88 -0
@@ 1,88 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+Realm is the visual exploration portal for cytrogen.icu. It replaces the root domain's previous blog with a Castlevania-inspired territory map. The blog lives at blog.cytrogen.icu.
+
+**Design language**: Deep cold tones (oklch blues), high-saturation orange as the sole accent, flat geometry, no strokes, no gradients, large color blocks.
+
+**Tech stack**: Pure HTML + CSS. Zero JavaScript. All SVG is inline. No build tools, no bundler, no framework.
+
+## Deployment
+
+Push-to-deploy via bare repo on VPS (accessible as `ssh vps-user`):
+
+```
+git push origin main
+```
+
+The post-receive hook at `~/repos/realm.git/hooks/post-receive` performs:
+1. Checkout to temp dir
+2. rsync to `/var/www/realm` (excludes `.git`, `caddy/`, `CLAUDE.md`)
+3. Push to local Sourcehut instance (`ssh://git@localhost:22222/~cytrogen/realm`)
+
+Caddy config lives at `/etc/caddy/Caddyfile` on the VPS (backup at `~/vps-deploy/Caddyfile`). Editing requires sudo. The `caddy/` directory in this repo contains reference snippets only.
+
+### VPS Infrastructure
+
+| Subdomain | Service | Backend |
+|---|---|---|
+| cytrogen.icu | Phase 1: 301 → blog.cytrogen.icu | Caddy redirect |
+| blog.cytrogen.icu | Hexo blog (static) | `/var/www/blog.cytrogen.icu` |
+| git.cytrogen.icu | Sourcehut git | localhost:5003 |
+| meta.cytrogen.icu | Sourcehut meta | localhost:5001 |
+| rss.cytrogen.icu | FreshRSS | localhost:8096 |
+| status.cytrogen.icu | Beszel | localhost:8090 |
+| mail.cytrogen.icu | Stalwart | localhost:8080 |
+
+Blog deploys via `~/repos/blog.git` → builds Hexo → `/var/www/blog.cytrogen.icu` → pushes to Sourcehut (`blog-local` + `blog-public`).
+
+## Architecture
+
+### CSS (`css/realm.css`)
+
+Single stylesheet using `@layer` for cascade control:
+```
+@layer reset, tokens, base, layout, components, utilities;
+```
+
+- **tokens**: oklch color variables, spacing, typography scales, motion durations on `:root`
+- **components**: SVG fill classes (`.svg-icon`, `.svg-interactive`), navigation, panels
+- Typed `@property` declarations at file top enable smooth color transitions on SVG fills
+- `prefers-reduced-motion` kills all transition durations globally
+
+Colors use oklch with relative color syntax (`oklch(from var(--accent) ...)`) for derived variants.
+
+### SVG Conventions
+
+All SVG assets are inline in the HTML DOM — never `<img>` or `<object>`.
+
+- Reusable shapes: `<symbol>` in a hidden sprite block, referenced via `<use href="#id">`
+- Decorative SVG: `aria-hidden="true" focusable="false"`
+- Informative SVG: `role="img" aria-labelledby="title-id"` with `<title>` as first child
+- Paths use CSS classes for fills — no inline `fill` attributes
+- No `stroke` attributes anywhere. No gradients. Fill-only flat geometry.
+- Use `viewBox` only — omit explicit `width`/`height` on `<symbol>` for responsive scaling
+
+### Page Structure
+
+- `index.html` — Realm entry, contains the SVG sprite `<defs>` block and navigation
+- `pages/contact.html` — 领地联络处 (Contact)
+- `caddy/` — Reference Caddyfile snippets (not deployed)
+
+## Migration Phases
+
+1. **Phase 1** (done): 301 redirect cytrogen.icu → blog.cytrogen.icu, blog files at `/var/www/blog.cytrogen.icu`
+2. **Phase 2** (current): CSS/SVG design system established in `css/realm.css`
+3. **Phase 3** (planned): Full scene layout — Castlevania-style layered environment or game-map navigation. CSS classes `.realm-scene`, `.realm-layer--far/mid/near` are pre-defined in the layout layer.
+4. **Phase 4** (planned): Realm goes live at cytrogen.icu root (serve from `/var/www/realm`), remove 301 redirects
+
+## Design Constraints
+
+- No JavaScript unless absolutely unavoidable
+- All interactivity via CSS (`:hover`, `:focus-visible`, transitions, `@property` animations)
+- SVG complexity must degrade gracefully — use container queries or media queries to hide detail at small sizes
+- Text-only fallback for terminals/Gemini-style access (planned: separate gopher/gemini protocol support)
+- Accessibility: skip decorative SVG via `aria-hidden`, label informative SVG, respect `prefers-reduced-motion`
A => caddy/phase1-redirect.example.Caddyfile +51 -0
@@ 1,51 @@
+# Phase 1: Blog migration — cytrogen.icu → blog.cytrogen.icu
+#
+# This is a reference copy of the redirect blocks deployed to /etc/caddy/Caddyfile.
+# The production Caddyfile also includes blocks for sourcehut, beszel, freshrss,
+# stalwart, etc. — only the realm-relevant portions are shown here.
+#
+# Status: DEPLOYED 2026-02-27
+
+# Blog now served from /var/www/blog.cytrogen.icu
+blog.cytrogen.icu {
+ root * /var/www/blog.cytrogen.icu
+ file_server
+ encode gzip
+
+ @static path *.css *.js *.jpg *.jpeg *.png *.gif *.ico *.svg *.webp *.woff *.woff2 *.ttf *.eot
+ header @static Cache-Control "public, max-age=2592000, immutable"
+
+ @html path *.html
+ header @html Cache-Control "public, max-age=3600, must-revalidate"
+
+ handle_errors {
+ @4xx expression `{err.status_code} >= 400 && {err.status_code} < 500`
+ handle @4xx {
+ rewrite * /{err.status_code}.html
+ file_server
+ }
+ @5xx expression `{err.status_code} >= 500`
+ handle @5xx {
+ rewrite * /{err.status_code}.html
+ file_server
+ }
+ }
+
+ try_files {path} {path}/ {path}.html
+}
+
+# 301 permanent redirect — root domain to blog subdomain
+cytrogen.icu, www.cytrogen.icu {
+ header Link `<https://blog.cytrogen.icu{uri}>; rel="canonical"`
+ header X-Robots-Tag "noindex, nofollow"
+ redir https://blog.cytrogen.icu{uri} permanent
+}
+
+# Phase 4: Replace the redirect block above with:
+#
+# cytrogen.icu, www.cytrogen.icu {
+# root * /var/www/realm
+# file_server
+# encode gzip
+# try_files {path} {path}/ {path}.html
+# }
A => css/realm.css +344 -0
@@ 1,344 @@
+/*
+ * Realm — Design System
+ * Castlevania-inspired: deep cold tones, orange accent, flat geometry
+ *
+ * Architecture: CSS @layer for cascade control
+ * Colors: oklch with relative color syntax
+ * SVG: fill-only, no stroke, CSS-controlled
+ */
+
+@layer reset, tokens, base, layout, components, utilities;
+
+/* ================================================================
+ @property — typed custom properties for smooth animation
+ ================================================================ */
+
+@property --fill-current {
+ syntax: "<color>";
+ inherits: true;
+ initial-value: oklch(0.85 0.02 240);
+}
+
+@property --fill-accent {
+ syntax: "<color>";
+ inherits: true;
+ initial-value: oklch(0.72 0.18 55);
+}
+
+/* ================================================================
+ RESET
+ ================================================================ */
+
+@layer reset {
+ *,
+ *::before,
+ *::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ }
+
+ html {
+ -webkit-text-size-adjust: 100%;
+ text-size-adjust: 100%;
+ }
+
+ img,
+ svg {
+ display: block;
+ max-width: 100%;
+ }
+
+ a {
+ color: inherit;
+ text-decoration: none;
+ }
+
+ ul,
+ ol {
+ list-style: none;
+ }
+}
+
+/* ================================================================
+ TOKENS
+ ================================================================ */
+
+@layer tokens {
+ :root {
+ /* === Background scale (deep cold) === */
+ --bg-abyss: oklch(0.10 0.02 260);
+ --bg-deep: oklch(0.15 0.03 255);
+ --bg-mid: oklch(0.22 0.04 250);
+ --bg-surface: oklch(0.28 0.04 245);
+ --bg-elevated: oklch(0.35 0.04 240);
+
+ /* === Text scale === */
+ --text-primary: oklch(0.85 0.02 240);
+ --text-secondary: oklch(0.60 0.03 250);
+ --text-dim: oklch(0.42 0.03 255);
+
+ /* === Accent: high-saturation orange (sole emphasis) === */
+ --accent: oklch(0.72 0.18 55);
+ --accent-bright: oklch(from var(--accent) calc(l + 0.1) c h);
+ --accent-dim: oklch(from var(--accent) calc(l - 0.15) c h);
+ --accent-glow: oklch(from var(--accent) l c h / 0.3);
+
+ /* === Spacing === */
+ --space-xs: 0.25rem;
+ --space-sm: 0.5rem;
+ --space-md: 1rem;
+ --space-lg: 2rem;
+ --space-xl: 4rem;
+
+ /* === Typography === */
+ --font-body: system-ui, -apple-system, "Segoe UI", sans-serif;
+ --font-mono: ui-monospace, "Cascadia Code", "Fira Code", monospace;
+ --text-sm: clamp(0.8rem, 0.75rem + 0.25vw, 0.875rem);
+ --text-base: clamp(0.95rem, 0.9rem + 0.25vw, 1.05rem);
+ --text-lg: clamp(1.2rem, 1rem + 1vw, 1.75rem);
+ --text-xl: clamp(1.8rem, 1.4rem + 2vw, 3rem);
+
+ /* === Motion === */
+ --ease-default: cubic-bezier(0.4, 0, 0.2, 1);
+ --ease-out: cubic-bezier(0, 0, 0.2, 1);
+ --duration-fast: 150ms;
+ --duration-normal: 300ms;
+ --duration-slow: 600ms;
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ :root {
+ --duration-fast: 0ms;
+ --duration-normal: 0ms;
+ --duration-slow: 0ms;
+ }
+ }
+}
+
+/* ================================================================
+ BASE
+ ================================================================ */
+
+@layer base {
+ html {
+ color-scheme: dark;
+ }
+
+ body {
+ font-family: var(--font-body);
+ font-size: var(--text-base);
+ line-height: 1.6;
+ color: var(--text-primary);
+ background-color: var(--bg-abyss);
+ min-height: 100dvh;
+ overflow-x: hidden;
+ }
+
+ h1,
+ h2,
+ h3 {
+ line-height: 1.2;
+ color: var(--text-primary);
+ }
+
+ h1 { font-size: var(--text-xl); }
+ h2 { font-size: var(--text-lg); }
+ h3 { font-size: var(--text-base); }
+
+ p {
+ color: var(--text-secondary);
+ }
+
+ code {
+ font-family: var(--font-mono);
+ font-size: 0.9em;
+ }
+
+ ::selection {
+ background: var(--accent-glow);
+ color: var(--text-primary);
+ }
+}
+
+/* ================================================================
+ LAYOUT
+ ================================================================ */
+
+@layer layout {
+ .realm {
+ display: grid;
+ place-items: center;
+ min-height: 100dvh;
+ padding: var(--space-lg);
+ }
+
+ .realm-container {
+ width: 100%;
+ max-width: 72rem;
+ margin-inline: auto;
+ padding-inline: var(--space-md);
+ }
+
+ /* Phase 3 scene viewport — reserves full-screen space */
+ .realm-scene {
+ position: relative;
+ width: 100%;
+ min-height: 100dvh;
+ overflow: hidden;
+ }
+
+ /* Scene depth layers (Phase 3) */
+ .realm-layer {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ }
+
+ .realm-layer--far { z-index: 1; }
+ .realm-layer--mid { z-index: 2; }
+ .realm-layer--near { z-index: 3; }
+ .realm-layer--ui { z-index: 10; }
+}
+
+/* ================================================================
+ COMPONENTS
+ ================================================================ */
+
+@layer components {
+ /* --- Navigation --- */
+
+ .realm-nav {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-md);
+ align-items: center;
+ text-align: center;
+ }
+
+ .realm-nav a {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-sm);
+ font-size: var(--text-lg);
+ color: var(--text-secondary);
+ transition: color var(--duration-normal) var(--ease-default);
+ }
+
+ .realm-nav a:hover,
+ .realm-nav a:focus-visible {
+ color: var(--accent);
+ }
+
+ .realm-nav a:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 4px;
+ }
+
+ /* --- Realm Title / Sigil --- */
+
+ .realm-sigil {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--space-lg);
+ margin-block-end: var(--space-xl);
+ }
+
+ .realm-sigil svg {
+ width: clamp(6rem, 15vw, 12rem);
+ height: auto;
+ }
+
+ .realm-sigil__title {
+ font-size: var(--text-xl);
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--text-primary);
+ }
+
+ .realm-sigil__subtitle {
+ font-size: var(--text-sm);
+ letter-spacing: 0.2em;
+ text-transform: uppercase;
+ color: var(--text-dim);
+ }
+
+ /* --- SVG Components --- */
+
+ /* Fill defaults: all SVG paths inherit from CSS, not inline attributes */
+ .svg-icon {
+ fill: var(--fill-current);
+ transition: fill var(--duration-normal) var(--ease-default);
+ }
+
+ .svg-icon--accent {
+ fill: var(--fill-accent);
+ }
+
+ /* Interactive SVG elements */
+ .svg-interactive {
+ cursor: pointer;
+ transition:
+ fill var(--duration-normal) var(--ease-default),
+ opacity var(--duration-fast) var(--ease-default),
+ transform var(--duration-normal) var(--ease-default);
+ transform-origin: center;
+ }
+
+ .svg-interactive:hover {
+ fill: var(--accent);
+ transform: scale(1.05);
+ }
+
+ /* Decorative divider */
+ .realm-divider {
+ width: 100%;
+ max-width: 20rem;
+ height: auto;
+ margin-inline: auto;
+ opacity: 0.3;
+ }
+
+ .realm-divider path {
+ fill: var(--text-dim);
+ }
+
+ /* --- Card / Panel --- */
+
+ .realm-panel {
+ background: var(--bg-surface);
+ padding: var(--space-lg);
+ container-type: inline-size;
+ }
+
+ @container (min-width: 30rem) {
+ .realm-panel {
+ padding: var(--space-xl);
+ }
+ }
+}
+
+/* ================================================================
+ UTILITIES
+ ================================================================ */
+
+@layer utilities {
+ .visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+ }
+
+ .text-accent { color: var(--accent); }
+ .text-dim { color: var(--text-dim); }
+ .fill-accent { fill: var(--accent); }
+ .fill-primary { fill: var(--text-primary); }
+}
A => index.html +94 -0
@@ 1,94 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Realm — cytrogen.icu</title>
+ <meta name="description" content="Realm — the visual exploration territory of cytrogen.icu">
+ <meta name="theme-color" content="#1a1a2e">
+ <link rel="stylesheet" href="/css/realm.css">
+</head>
+<body>
+
+ <!-- ============================================================
+ SVG Sprite Definitions (hidden from rendering and AT)
+ All reusable symbols defined here, referenced via <use href>
+ ============================================================ -->
+ <svg aria-hidden="true" focusable="false" style="position:absolute;width:0;height:0;overflow:hidden">
+ <defs>
+ <!-- Realm Sigil: geometric castle tower silhouette -->
+ <symbol id="sigil-tower" viewBox="0 0 120 160">
+ <path class="svg-icon" d="
+ M60 0 L70 20 H80 V40 H90 V60 H100 V160 H20 V60 H30 V40 H40 V20 H50 Z
+ "/>
+ <rect class="svg-icon" x="50" y="100" width="20" height="60"/>
+ <rect class="svg-icon--accent" x="45" y="70" width="10" height="15"/>
+ <rect class="svg-icon--accent" x="65" y="70" width="10" height="15"/>
+ <rect class="svg-icon--accent" x="55" y="50" width="10" height="10"/>
+ </symbol>
+
+ <!-- Navigation arrow: right-pointing chevron -->
+ <symbol id="icon-arrow" viewBox="0 0 24 24">
+ <path class="svg-icon" d="M8 4 L18 12 L8 20 Z"/>
+ </symbol>
+
+ <!-- Decorative divider: geometric battlement pattern -->
+ <symbol id="divider-battlement" viewBox="0 0 200 12">
+ <path class="svg-icon" d="
+ M0 12 V6 H10 V0 H20 V6 H30 V0 H40 V6 H50 V0 H60 V6
+ H70 V0 H80 V6 H90 V0 H100 V6 H110 V0 H120 V6
+ H130 V0 H140 V6 H150 V0 H160 V6 H170 V0 H180 V6
+ H190 V0 H200 V6 V12 Z
+ "/>
+ </symbol>
+
+ <!-- Side door icon: arched doorway -->
+ <symbol id="icon-door" viewBox="0 0 40 60">
+ <path class="svg-icon" d="
+ M0 60 V15 Q0 0 20 0 Q40 0 40 15 V60 Z
+ "/>
+ <circle class="svg-icon--accent" cx="28" cy="35" r="2.5"/>
+ </symbol>
+ </defs>
+ </svg>
+
+ <main class="realm">
+ <!-- Sigil and title -->
+ <header class="realm-sigil">
+ <svg role="img" aria-labelledby="sigil-title" width="120" height="160">
+ <title id="sigil-title">Realm sigil — a castle tower</title>
+ <use href="#sigil-tower"/>
+ </svg>
+ <h1 class="realm-sigil__title">Realm</h1>
+ <p class="realm-sigil__subtitle">cytrogen.icu</p>
+ </header>
+
+ <!-- Divider -->
+ <svg class="realm-divider" aria-hidden="true" focusable="false">
+ <use href="#divider-battlement"/>
+ </svg>
+
+ <!-- Navigation: realm destinations -->
+ <nav class="realm-nav" aria-label="Realm navigation">
+ <a href="https://blog.cytrogen.icu">
+ <svg aria-hidden="true" focusable="false" width="20" height="20">
+ <use href="#icon-door"/>
+ </svg>
+ 城堡入口 — Blog
+ </a>
+ <a href="/pages/contact.html">
+ <svg aria-hidden="true" focusable="false" width="16" height="16">
+ <use href="#icon-arrow"/>
+ </svg>
+ 侧门 — Contact
+ </a>
+ </nav>
+
+ <!-- Divider -->
+ <svg class="realm-divider" aria-hidden="true" focusable="false" style="margin-top:var(--space-xl)">
+ <use href="#divider-battlement"/>
+ </svg>
+ </main>
+
+</body>
+</html>
A => pages/contact.html +32 -0
@@ 1,32 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Contact — Realm</title>
+ <meta name="description" content="领地联络处 — Contact the Realm">
+ <meta name="theme-color" content="#1a1a2e">
+ <link rel="stylesheet" href="/css/realm.css">
+</head>
+<body>
+
+ <main class="realm">
+ <header class="realm-sigil">
+ <h1 class="realm-sigil__title">领地联络处</h1>
+ <p class="realm-sigil__subtitle">Contact</p>
+ </header>
+
+ <div class="realm-panel">
+ <p>Reach out via the channels listed below.</p>
+ <!-- Add contact methods here -->
+ </div>
+
+ <nav class="realm-nav" style="margin-top:var(--space-xl)">
+ <a href="/">
+ ← 返回领地
+ </a>
+ </nav>
+ </main>
+
+</body>
+</html>
A => robots.txt +4 -0
@@ 1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://cytrogen.icu/sitemap.xml