~cytrogen/realm

4691e5f5dcf3fab174c95a5e5cd6414a05bdd329 — Cytrogen a month ago
Initialize Realm: Phase 1 redirect + Phase 2 design system

- Caddy 301 redirect config: cytrogen.icu → blog.cytrogen.icu
- CSS design system: oklch color tokens, @layer cascade, @property animations
- Base index.html with inline SVG sprite (sigil, navigation, dividers)
- Contact page placeholder
7 files changed, 614 insertions(+), 0 deletions(-)

A .gitignore
A CLAUDE.md
A caddy/phase1-redirect.example.Caddyfile
A css/realm.css
A index.html
A pages/contact.html
A robots.txt
A  => .gitignore +1 -0
@@ 1,1 @@
.claude/

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