Add dark theme and secret key creation script

This commit is contained in:
2026-09-18 23:03:05 +03:00
parent d1d594674f
commit d048ef82b7
8 changed files with 252 additions and 10 deletions
+20 -1
View File
@@ -4,7 +4,20 @@ A Dockerized publishing service for eternityproject.fi. It provides local accoun
## Run it
1. Copy `.env.example` to `.env` and replace `SECRET_KEY` with a long random string.
1. Copy `.env.example` to `.env` and replace `SECRET_KEY` with a long random string. Generate one with:
```powershell
# Windows PowerShell
.\scripts\generate-secret-key.ps1
```
```sh
# Linux, macOS, or any POSIX shell
sh ./scripts/generate-secret-key.sh
```
Paste the printed value into `SECRET_KEY` in `.env`. The scripts only print a value; they never modify `.env`.
2. Build and start the service:
```sh
@@ -31,6 +44,12 @@ Posts are public by default. Authors can select **Members only** while writing o
Administrators can freeze member accounts from **Accounts**. Frozen members cannot sign in and are signed out on their next protected request; administrators can unfreeze them later. Administrators can also permanently delete a member account. Deleting an account also permanently removes its posts, uploaded images, and outstanding reset or verification tokens.
## Theming
The site ships with a light theme and a dark "wasteland" variant. The **WASTELAND** / **DAYLIGHT** button in the navigation toggles between them and remembers the choice in the browser's local storage; an inline script applies the saved theme before the page renders to avoid a flash of the wrong theme. Colors for both variants live as CSS custom properties in [static/css/site.css](static/css/site.css), overridden under the `[data-theme="dark"]` selector.
A framework-agnostic copy of the theme is available under [themes/](themes/) for reuse on other web pages: [themes/eternity-theme.css](themes/eternity-theme.css) contains the core design system (header, buttons, hero, cards, forms, code blocks, flashes) with no Flask or Jinja dependencies, and [themes/demo.html](themes/demo.html) is a standalone starter template demonstrating the components and the dark-mode toggle.
## Accounts and MFA
The initial administrator account is `admin` with password `admin`, as requested for first-run access. Sign in, scan the displayed QR code with any iPhone or Android TOTP authenticator, and change this password before exposing the service to the internet. New registrations are held for approval in **Accounts**; accepted users must enroll a TOTP authenticator before they can publish.
BIN
View File
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
# Prints a cryptographically random SECRET_KEY value; does not modify .env.
$bytes = [byte[]]::new(32)
$rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new()
try {
$rng.GetBytes($bytes)
} finally {
$rng.Dispose()
}
($bytes | ForEach-Object { $_.ToString("x2") }) -join ""
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
set -eu
# Prints a cryptographically random SECRET_KEY value; does not modify .env.
if command -v python3 >/dev/null 2>&1; then
python3 -c 'import secrets; print(secrets.token_hex(32))'
elif command -v openssl >/dev/null 2>&1; then
openssl rand -hex 32
else
printf '%s\n' "Neither python3 nor openssl is available to generate a secret." >&2
exit 1
fi
+12 -9
View File
@@ -1,8 +1,11 @@
:root { --ink:#11212b; --paper:#f1f1e9; --acid:#c7ef4b; --orange:#ff6b35; --line:#b9c5bc; --muted:#59706b; }
:root { --ink:#11212b; --paper:#f1f1e9; --acid:#c7ef4b; --orange:#ff6b35; --line:#b9c5bc; --muted:#59706b; --panel:rgba(241,241,233,.8); --field:#fbfbf6; --backdrop:rgba(17,33,43,.85); --flash-error:#ffe3d9; --grid:17,33,43; --grid-a1:.05; --grid-a2:.04; }
[data-theme="dark"] { --ink:#e7e2d3; --paper:#161210; --acid:#8fae3d; --orange:#e4632c; --line:#4a3f33; --muted:#a89a86; --panel:rgba(22,18,16,.82); --field:#211b17; --backdrop:rgba(8,6,5,.9); --flash-error:#3a1c14; --grid:228,98,44; --grid-a1:.07; --grid-a2:.05; }
* { box-sizing:border-box; }
html { scroll-behavior:smooth; }
body { margin:0; background:var(--paper); color:var(--ink); font-family:"Space Grotesk", sans-serif; }
body::before { content:""; position:fixed; inset:0; pointer-events:none; opacity:.4; background-image:linear-gradient(90deg, transparent 49.5%, rgba(17,33,43,.05) 50%, transparent 50.5%),linear-gradient(rgba(17,33,43,.04) 1px, transparent 1px); background-size:70px 70px, 7px 7px; }
body { margin:0; background:var(--paper); color:var(--ink); font-family:"Space Grotesk", sans-serif; transition:background .2s, color .2s; }
body::before { content:""; position:fixed; inset:0; pointer-events:none; opacity:.4; background-image:linear-gradient(90deg, transparent 49.5%, rgba(var(--grid),var(--grid-a1)) 50%, transparent 50.5%),linear-gradient(rgba(var(--grid),var(--grid-a2)) 1px, transparent 1px); background-size:70px 70px, 7px 7px; }
[data-theme="dark"] body::after { content:""; position:fixed; inset:0; pointer-events:none; opacity:.5; mix-blend-mode:overlay; background-image:radial-gradient(circle at 12% 20%, rgba(143,174,61,.12), transparent 35%), radial-gradient(circle at 85% 75%, rgba(228,98,44,.14), transparent 40%); }
.theme-toggle { border:1px solid var(--ink); background:transparent; color:inherit; padding:6px 9px; font:11px "DM Mono", monospace; cursor:pointer; letter-spacing:.5px; }.theme-toggle:hover { background:var(--acid); color:var(--paper); }
a { color:inherit; text-decoration:none; } .site-header, main, footer { position:relative; z-index:1; }
.site-header { min-height:76px; padding:16px max(5vw, 24px); border-bottom:1px solid var(--ink); display:flex; align-items:center; justify-content:space-between; gap:24px; }
.brand { display:flex; gap:10px; align-items:center; font-family:"DM Mono", monospace; line-height:1; }.brand strong { display:block; letter-spacing:1px; font-size:16px; }.brand small { font-size:9px; color:var(--muted); }
@@ -11,12 +14,12 @@ nav { display:flex; align-items:center; gap:22px; font-family:"DM Mono", monospa
.button { display:inline-flex; align-items:center; justify-content:center; gap:18px; background:var(--ink); color:var(--paper); padding:13px 17px; border:1px solid var(--ink); font:500 13px "DM Mono", monospace; cursor:pointer; }.button:hover { background:var(--acid); color:var(--ink); }.compact { padding:9px 12px; }
.hero { min-height:620px; padding:clamp(70px, 12vw, 150px) max(5vw, 24px) 75px; border-bottom:1px solid var(--ink); position:relative; overflow:hidden; }.signal-label, .section-head, .post-meta, .byline { font:11px "DM Mono", monospace; letter-spacing:.7px; }.signal-label { color:var(--orange); }.hero h1 { max-width:820px; margin:20px 0; font-size:clamp(52px, 9vw, 132px); line-height:.83; letter-spacing:0; font-weight:600; }.hero h1 em { font-family:Georgia, serif; font-weight:400; }.hero p { max-width:510px; color:var(--muted); font-size:18px; line-height:1.45; margin:28px 0; }.scope { position:absolute; right:8%; bottom:-95px; width:310px; height:310px; border:2px solid var(--orange); border-radius:50%; display:grid; place-items:center; font:100px Georgia, serif; color:var(--orange); }.scope span { position:absolute; width:100%; height:1px; background:var(--orange); transform:rotate(45deg); }.scope span:nth-child(2){ transform:rotate(90deg) }.scope span:nth-child(3){ transform:rotate(135deg) }
.journal, .editor-wrap { padding:40px max(5vw, 24px) 90px; }.section-head { display:flex; justify-content:space-between; padding-bottom:14px; border-bottom:1px solid var(--ink); margin-bottom:18px; }.post-grid { display:grid; grid-template-columns:repeat(3, 1fr); border-top:1px solid var(--ink); border-left:1px solid var(--ink); }.post-card { min-height:320px; padding:21px; border-right:1px solid var(--ink); border-bottom:1px solid var(--ink); display:flex; flex-direction:column; transition:background .2s; }.post-card:hover { background:var(--acid); }.post-meta { display:flex; justify-content:space-between; color:var(--muted); }.post-card h2 { font-size:29px; line-height:1; margin:28px 0 14px; }.post-card h2 a:hover { text-decoration:underline; text-decoration-thickness:2px; }.post-card p { line-height:1.45; margin:0; color:var(--muted); }.post-foot { margin-top:auto; display:flex; justify-content:space-between; align-items:center; padding-top:20px; font:10px "DM Mono", monospace; }.post-foot a { font:30px "DM Mono", monospace; }.empty-state { min-height:300px; padding:64px 0; text-align:center; border-bottom:1px solid var(--ink); }.empty-icon { font:42px "DM Mono", monospace; color:var(--orange); }.empty-state h2 { margin:10px 0 0; font-size:31px; }.empty-state p { color:var(--muted); margin:8px 0 24px; }
.auth-layout { min-height:calc(100vh - 160px); padding:70px max(10vw, 24px); display:grid; grid-template-columns:1fr minmax(280px, 390px); gap:10vw; align-items:center; }.auth-copy h1 { font-size:clamp(45px, 6vw, 82px); line-height:.9; margin:15px 0; }.auth-copy p { max-width:380px; font-size:18px; line-height:1.5; color:var(--muted); }.auth-form, .editor-form { display:grid; gap:19px; }.auth-form { padding:27px; border:1px solid var(--ink); background:rgba(241,241,233,.8); }.auth-form label, .editor-form label { display:grid; gap:7px; font:11px "DM Mono", monospace; }input, textarea { width:100%; resize:vertical; background:#fbfbf6; border:1px solid var(--ink); border-radius:0; padding:12px; color:var(--ink); font:15px "Space Grotesk", sans-serif; }input:focus, textarea:focus { outline:3px solid var(--acid); outline-offset:1px; }.form-switch { font-size:13px; color:var(--muted); }.form-switch a { color:var(--ink); text-decoration:underline; }.editor-form { max-width:900px; }.two-col { display:grid; grid-template-columns:1fr 2fr; gap:19px; }
.auth-layout { min-height:calc(100vh - 160px); padding:70px max(10vw, 24px); display:grid; grid-template-columns:1fr minmax(280px, 390px); gap:10vw; align-items:center; }.auth-copy h1 { font-size:clamp(45px, 6vw, 82px); line-height:.9; margin:15px 0; }.auth-copy p { max-width:380px; font-size:18px; line-height:1.5; color:var(--muted); }.auth-form, .editor-form { display:grid; gap:19px; }.auth-form { padding:27px; border:1px solid var(--ink); background:var(--panel); }.auth-form label, .editor-form label { display:grid; gap:7px; font:11px "DM Mono", monospace; }input, textarea { width:100%; resize:vertical; background:var(--field); border:1px solid var(--ink); border-radius:0; padding:12px; color:var(--ink); font:15px "Space Grotesk", sans-serif; }input:focus, textarea:focus { outline:3px solid var(--acid); outline-offset:1px; }.form-switch { font-size:13px; color:var(--muted); }.form-switch a { color:var(--ink); text-decoration:underline; }.editor-form { max-width:900px; }.two-col { display:grid; grid-template-columns:1fr 2fr; gap:19px; }
.step-images { margin:0; padding:16px; border:1px solid var(--ink); }.step-images legend { padding:0 5px; font:11px "DM Mono", monospace; }.step-images p { margin:0 0 15px; color:var(--muted); font-size:13px; line-height:1.45; }.step-images code { color:var(--orange); font:12px "DM Mono", monospace; }.step-image-row { display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:12px; }.add-step-image { justify-self:start; border:1px solid var(--ink); background:transparent; padding:8px 10px; font:11px "DM Mono", monospace; cursor:pointer; }.add-step-image:hover { background:var(--acid); }
.post-visibility { grid-template-columns:auto 1fr; align-items:center; column-gap:9px; padding:12px; border:1px solid var(--ink); background:rgba(241,241,233,.8); cursor:pointer; }.post-visibility input { width:16px; height:16px; margin:0; accent-color:var(--orange); }.post-visibility small { grid-column:2; color:var(--muted); font:12px "Space Grotesk", sans-serif; letter-spacing:0; text-transform:none; }
.article { max-width:850px; margin:0 auto; padding:80px 24px 100px; }.article h1 { font-size:clamp(48px, 7vw, 90px); line-height:.93; margin:23px 0; }.article-lead { border-left:4px solid var(--orange); padding-left:18px; max-width:720px; font-size:23px; line-height:1.35; }.byline { padding:20px 0; border-top:1px solid var(--ink); border-bottom:1px solid var(--ink); margin:35px 0; }.article-image-button { display:block; max-width:680px; width:100%; margin:0 0 30px; padding:0; border:0; background:none; cursor:zoom-in; }.article-image { display:block; width:100%; max-height:430px; object-fit:cover; border:1px solid var(--ink); }.inline-image { max-width:680px; margin:28px 0; }.inline-image .article-image-button { margin:0; }.inline-image figcaption { padding-top:8px; color:var(--muted); font:12px "DM Mono", monospace; }.image-dialog { width:min(94vw, 1200px); max-width:none; padding:0; border:1px solid var(--ink); background:var(--ink); }.image-dialog::backdrop { background:rgba(17,33,43,.85); }.image-dialog img { display:block; width:100%; max-height:88vh; object-fit:contain; }.dialog-close { position:absolute; top:10px; right:10px; border:1px solid var(--paper); background:var(--ink); color:var(--paper); padding:8px 10px; font:12px "DM Mono", monospace; cursor:pointer; }.article-body { max-width:680px; font-size:18px; line-height:1.7; margin-bottom:35px; }.article-text { white-space:pre-wrap; }
.code-block { margin:28px 0; border:1px solid var(--ink); background:var(--ink); }.code-block-head { display:flex; align-items:center; justify-content:space-between; min-height:38px; padding:7px 10px 7px 14px; color:var(--paper); font:11px "DM Mono", monospace; letter-spacing:.7px; text-transform:uppercase; }.copy-code { border:1px solid var(--paper); background:transparent; color:var(--paper); padding:5px 8px; font:11px "DM Mono", monospace; cursor:pointer; }.copy-code:hover { border-color:var(--acid); background:var(--acid); color:var(--ink); }.code-block pre { margin:0; padding:18px; overflow:auto; background:#fbfbf6; color:var(--ink); font:14px/1.55 "DM Mono", monospace; white-space:pre; }.code-block code { font:inherit; }
.flash { margin:16px max(5vw, 24px) 0; padding:11px 14px; font:12px "DM Mono", monospace; border:1px solid var(--ink); }.flash.error { border-color:var(--orange); background:#ffe3d9; }.flash.success { background:var(--acid); }footer { border-top:1px solid var(--ink); padding:23px max(5vw, 24px); display:flex; justify-content:space-between; font:10px "DM Mono", monospace; color:var(--muted); }
.post-visibility { grid-template-columns:auto 1fr; align-items:center; column-gap:9px; padding:12px; border:1px solid var(--ink); background:var(--panel); cursor:pointer; }.post-visibility input { width:16px; height:16px; margin:0; accent-color:var(--orange); }.post-visibility small { grid-column:2; color:var(--muted); font:12px "Space Grotesk", sans-serif; letter-spacing:0; text-transform:none; }
.article { max-width:850px; margin:0 auto; padding:80px 24px 100px; }.article h1 { font-size:clamp(48px, 7vw, 90px); line-height:.93; margin:23px 0; }.article-lead { border-left:4px solid var(--orange); padding-left:18px; max-width:720px; font-size:23px; line-height:1.35; }.byline { padding:20px 0; border-top:1px solid var(--ink); border-bottom:1px solid var(--ink); margin:35px 0; }.article-image-button { display:block; max-width:680px; width:100%; margin:0 0 30px; padding:0; border:0; background:none; cursor:zoom-in; }.article-image { display:block; width:100%; max-height:430px; object-fit:cover; border:1px solid var(--ink); }.inline-image { max-width:680px; margin:28px 0; }.inline-image .article-image-button { margin:0; }.inline-image figcaption { padding-top:8px; color:var(--muted); font:12px "DM Mono", monospace; }.image-dialog { width:min(94vw, 1200px); max-width:none; padding:0; border:1px solid var(--ink); background:var(--ink); }.image-dialog::backdrop { background:var(--backdrop); }.image-dialog img { display:block; width:100%; max-height:88vh; object-fit:contain; }.dialog-close { position:absolute; top:10px; right:10px; border:1px solid var(--paper); background:var(--ink); color:var(--paper); padding:8px 10px; font:12px "DM Mono", monospace; cursor:pointer; }.article-body { max-width:680px; font-size:18px; line-height:1.7; margin-bottom:35px; }.article-text { white-space:pre-wrap; }
.code-block { margin:28px 0; border:1px solid var(--ink); background:var(--ink); }.code-block-head { display:flex; align-items:center; justify-content:space-between; min-height:38px; padding:7px 10px 7px 14px; color:var(--paper); font:11px "DM Mono", monospace; letter-spacing:.7px; text-transform:uppercase; }.copy-code { border:1px solid var(--paper); background:transparent; color:var(--paper); padding:5px 8px; font:11px "DM Mono", monospace; cursor:pointer; }.copy-code:hover { border-color:var(--acid); background:var(--acid); color:var(--ink); }.code-block pre { margin:0; padding:18px; overflow:auto; background:var(--field); color:var(--ink); font:14px/1.55 "DM Mono", monospace; white-space:pre; }.code-block code { font:inherit; }
.flash { margin:16px max(5vw, 24px) 0; padding:11px 14px; font:12px "DM Mono", monospace; border:1px solid var(--ink); }.flash.error { border-color:var(--orange); background:var(--flash-error); }.flash.success { background:var(--acid); }footer { border-top:1px solid var(--ink); padding:23px max(5vw, 24px); display:flex; justify-content:space-between; font:10px "DM Mono", monospace; color:var(--muted); }
.mfa-qr { display:block; width:200px; max-width:100%; margin-top:28px; border:8px solid #fff; image-rendering:pixelated; }.mfa-manual { margin-top:18px; color:var(--muted); font:12px "DM Mono", monospace; }.mfa-manual summary { cursor:pointer; }.mfa-secret { display:block; width:max-content; max-width:100%; overflow-wrap:anywhere; margin-top:10px; padding:14px; border:1px solid var(--ink); background:var(--acid); color:var(--ink); font:14px "DM Mono", monospace; }.account-table { border:1px solid var(--ink); }.account-row { display:grid; grid-template-columns:2fr 1.5fr 1fr 1fr; gap:16px; align-items:center; min-height:64px; padding:12px 16px; border-bottom:1px solid var(--ink); font:13px "DM Mono", monospace; }.account-row:last-child { border-bottom:0; }.account-head { min-height:auto; padding:10px 16px; background:var(--ink); color:var(--paper); font-size:10px; }.account-row small { color:var(--orange); font:10px "DM Mono", monospace; }.account-row .email { display:block; margin-top:5px; color:var(--muted); overflow-wrap:anywhere; }
.account-actions { display:flex; flex-wrap:wrap; align-items:center; gap:6px; }.account-actions form { margin:0; }.account-actions strong { color:var(--orange); font:10px "DM Mono", monospace; }.delete-account { border-color:var(--orange); background:#ffe3d9; color:var(--ink); }.delete-account:hover { background:var(--orange); }
.account-actions { display:flex; flex-wrap:wrap; align-items:center; gap:6px; }.account-actions form { margin:0; }.account-actions strong { color:var(--orange); font:10px "DM Mono", monospace; }.delete-account { border-color:var(--orange); background:var(--flash-error); color:var(--ink); }.delete-account:hover { background:var(--orange); }
@media (max-width:720px) { .site-header { align-items:flex-start; }.site-header nav { justify-content:flex-end; flex-wrap:wrap; gap:10px 15px; }.hero { min-height:560px; }.scope { width:220px; height:220px; right:-45px; }.post-grid, .auth-layout, .two-col, .step-image-row { grid-template-columns:1fr; }.auth-layout { gap:40px; padding:60px 24px; }footer { flex-wrap:wrap; gap:8px 16px; }.user-chip { display:none; }.account-row { grid-template-columns:1fr 1fr; }.account-head { display:none; } }
+15
View File
@@ -9,6 +9,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/site.css') }}">
<script>(function(){var t=localStorage.getItem('ep-theme');if(t==='dark'){document.documentElement.setAttribute('data-theme','dark');}})();</script>
</head>
<body>
<header class="site-header">
@@ -30,6 +31,7 @@
<a href="{{ url_for('login') }}">Sign in</a>
<a class="button compact" href="{{ url_for('register') }}">Create account</a>
{% endif %}
<button type="button" class="theme-toggle" id="theme-toggle" aria-label="Toggle dark theme">WASTELAND</button>
</nav>
</header>
@@ -47,5 +49,18 @@
<span>CODE / CIRCUITS / CONTINUITY</span>
<span>HELSINKI, FI</span>
</footer>
<script>
(function(){
var root=document.documentElement,btn=document.getElementById('theme-toggle');
function label(){btn.textContent=root.getAttribute('data-theme')==='dark'?'DAYLIGHT':'WASTELAND';}
label();
btn.addEventListener('click', function(){
var dark=root.getAttribute('data-theme')==='dark';
if (dark) { root.removeAttribute('data-theme'); localStorage.setItem('ep-theme','light'); }
else { root.setAttribute('data-theme','dark'); localStorage.setItem('ep-theme','dark'); }
label();
});
})();
</script>
</body>
</html>
+77
View File
@@ -0,0 +1,77 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Eternity Theme — Starter Template</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="eternity-theme.css">
<script>(function(){if(localStorage.getItem('ep-theme')==='dark'){document.documentElement.setAttribute('data-theme','dark');}})();</script>
</head>
<body>
<header class="site-header">
<strong style="font-family:'DM Mono',monospace;letter-spacing:1px;">YOUR BRAND</strong>
<nav>
<a href="#">Home</a>
<a href="#">About</a>
<a class="button compact" href="#">Get started</a>
<button type="button" class="theme-toggle" id="theme-toggle">WASTELAND</button>
</nav>
</header>
<main>
<section class="hero">
<span class="signal-label">/// STARTER TEMPLATE</span>
<h1>Reusable <em>post-apocalyptic</em> theme.</h1>
<p>Drop <code>eternity-theme.css</code> into any site, wire up the toggle script, and switch between the daylight and wasteland palettes with a single <code>data-theme</code> attribute.</p>
<button class="button">Primary action</button>
</section>
<section style="padding:40px max(5vw,24px);">
<div class="card-grid">
<div class="card"><h2>Card one</h2><p>Short description of a feature or article.</p></div>
<div class="card"><h2>Card two</h2><p>Short description of a feature or article.</p></div>
<div class="card"><h2>Card three</h2><p>Short description of a feature or article.</p></div>
</div>
</section>
<section style="padding:0 max(5vw,24px) 40px;max-width:600px;">
<form class="panel">
<label>Name<input type="text" placeholder="Jane Doe"></label>
<label>Message<textarea rows="4" placeholder="Say something..."></textarea></label>
<button class="button" type="submit">Send</button>
</form>
<div class="flash success" style="margin-top:16px;">This is a success message.</div>
<div class="flash error">This is an error message.</div>
</section>
<section style="padding:0 max(5vw,24px) 60px;max-width:700px;">
<div class="code-block">
<div class="code-block-head"><span>example.js</span><span>copy</span></div>
<pre><code>console.log("themed code block");</code></pre>
</div>
</section>
</main>
<footer>
<span>YOUR BRAND</span>
<span>MADE WITH ETERNITY THEME</span>
</footer>
<script>
(function(){
var root=document.documentElement,btn=document.getElementById('theme-toggle');
function label(){btn.textContent=root.getAttribute('data-theme')==='dark'?'DAYLIGHT':'WASTELAND';}
label();
btn.addEventListener('click', function(){
var dark=root.getAttribute('data-theme')==='dark';
if (dark) { root.removeAttribute('data-theme'); localStorage.setItem('ep-theme','light'); }
else { root.setAttribute('data-theme','dark'); localStorage.setItem('ep-theme','dark'); }
label();
});
})();
</script>
</body>
</html>
+107
View File
@@ -0,0 +1,107 @@
/* Eternity Project theme — portable core styles.
Usage: <html data-theme="dark"> to activate the wasteland variant,
or toggle the attribute at runtime and persist the choice yourself. */
:root {
--ink:#11212b; --paper:#f1f1e9; --acid:#c7ef4b; --orange:#ff6b35;
--line:#b9c5bc; --muted:#59706b;
--panel:rgba(241,241,233,.8); --field:#fbfbf6; --backdrop:rgba(17,33,43,.85);
--flash-error:#ffe3d9; --grid:17,33,43; --grid-a1:.05; --grid-a2:.04;
}
[data-theme="dark"] {
--ink:#e7e2d3; --paper:#161210; --acid:#8fae3d; --orange:#e4632c;
--line:#4a3f33; --muted:#a89a86;
--panel:rgba(22,18,16,.82); --field:#211b17; --backdrop:rgba(8,6,5,.9);
--flash-error:#3a1c14; --grid:228,98,44; --grid-a1:.07; --grid-a2:.05;
}
* { box-sizing:border-box; }
html { scroll-behavior:smooth; }
body {
margin:0; background:var(--paper); color:var(--ink);
font-family:"Space Grotesk", sans-serif; transition:background .2s, color .2s;
}
body::before {
content:""; position:fixed; inset:0; pointer-events:none; opacity:.4;
background-image:
linear-gradient(90deg, transparent 49.5%, rgba(var(--grid),var(--grid-a1)) 50%, transparent 50.5%),
linear-gradient(rgba(var(--grid),var(--grid-a2)) 1px, transparent 1px);
background-size:70px 70px, 7px 7px;
}
[data-theme="dark"] body::after {
content:""; position:fixed; inset:0; pointer-events:none; opacity:.5; mix-blend-mode:overlay;
background-image:
radial-gradient(circle at 12% 20%, rgba(143,174,61,.12), transparent 35%),
radial-gradient(circle at 85% 75%, rgba(228,98,44,.14), transparent 40%);
}
a { color:inherit; text-decoration:none; }
/* header / nav */
.site-header {
min-height:76px; padding:16px max(5vw, 24px); border-bottom:1px solid var(--ink);
display:flex; align-items:center; justify-content:space-between; gap:24px; position:relative; z-index:1;
}
nav { display:flex; align-items:center; gap:22px; font-family:"DM Mono", monospace; font-size:12px; }
nav a:hover { color:var(--orange); }
/* buttons */
.button {
display:inline-flex; align-items:center; justify-content:center; gap:18px;
background:var(--ink); color:var(--paper); padding:13px 17px; border:1px solid var(--ink);
font:500 13px "DM Mono", monospace; cursor:pointer;
}
.button:hover { background:var(--acid); color:var(--ink); }
.button.compact { padding:9px 12px; }
.theme-toggle {
border:1px solid var(--ink); background:transparent; color:inherit; padding:6px 9px;
font:11px "DM Mono", monospace; cursor:pointer; letter-spacing:.5px;
}
.theme-toggle:hover { background:var(--acid); color:var(--paper); }
/* hero */
.hero {
min-height:420px; padding:clamp(70px, 12vw, 150px) max(5vw, 24px) 75px;
border-bottom:1px solid var(--ink); position:relative; overflow:hidden; z-index:1;
}
.signal-label { color:var(--orange); font:11px "DM Mono", monospace; letter-spacing:.7px; }
.hero h1 { max-width:820px; margin:20px 0; font-size:clamp(40px, 9vw, 110px); line-height:.88; font-weight:600; }
.hero h1 em { font-family:Georgia, serif; font-weight:400; }
.hero p { max-width:510px; color:var(--muted); font-size:18px; line-height:1.45; margin:28px 0; }
/* cards / grid */
.card-grid { display:grid; grid-template-columns:repeat(3, 1fr); border-top:1px solid var(--ink); border-left:1px solid var(--ink); }
.card { min-height:260px; padding:21px; border-right:1px solid var(--ink); border-bottom:1px solid var(--ink); display:flex; flex-direction:column; transition:background .2s; }
.card:hover { background:var(--acid); }
.card h2 { font-size:26px; line-height:1; margin:22px 0 12px; }
.card p { line-height:1.45; margin:0; color:var(--muted); }
/* forms */
.panel { padding:27px; border:1px solid var(--ink); background:var(--panel); display:grid; gap:19px; }
label { display:grid; gap:7px; font:11px "DM Mono", monospace; }
input, textarea {
width:100%; resize:vertical; background:var(--field); border:1px solid var(--ink);
border-radius:0; padding:12px; color:var(--ink); font:15px "Space Grotesk", sans-serif;
}
input:focus, textarea:focus { outline:3px solid var(--acid); outline-offset:1px; }
/* code blocks */
.code-block { margin:28px 0; border:1px solid var(--ink); background:var(--ink); }
.code-block-head {
display:flex; align-items:center; justify-content:space-between; min-height:38px;
padding:7px 10px 7px 14px; color:var(--paper); font:11px "DM Mono", monospace;
letter-spacing:.7px; text-transform:uppercase;
}
.code-block pre { margin:0; padding:18px; overflow:auto; background:var(--field); color:var(--ink); font:14px/1.55 "DM Mono", monospace; white-space:pre; }
/* flash / alerts */
.flash { margin:16px 0; padding:11px 14px; font:12px "DM Mono", monospace; border:1px solid var(--ink); }
.flash.error { border-color:var(--orange); background:var(--flash-error); }
.flash.success { background:var(--acid); }
footer { border-top:1px solid var(--ink); padding:23px max(5vw, 24px); display:flex; justify-content:space-between; font:10px "DM Mono", monospace; color:var(--muted); position:relative; z-index:1; }
@media (max-width:720px) {
.card-grid { grid-template-columns:1fr; }
.site-header { flex-wrap:wrap; }
}