diff --git a/README.md b/README.md index 82bd26a..4a90fc7 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/data/eternity.db b/data/eternity.db index a3e1a8e..28be4f3 100644 Binary files a/data/eternity.db and b/data/eternity.db differ diff --git a/scripts/generate-secret-key.ps1 b/scripts/generate-secret-key.ps1 new file mode 100644 index 0000000..b730361 --- /dev/null +++ b/scripts/generate-secret-key.ps1 @@ -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 "" diff --git a/scripts/generate-secret-key.sh b/scripts/generate-secret-key.sh new file mode 100644 index 0000000..d6b544c --- /dev/null +++ b/scripts/generate-secret-key.sh @@ -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 diff --git a/static/css/site.css b/static/css/site.css index b3dfe81..74fe3c6 100644 --- a/static/css/site.css +++ b/static/css/site.css @@ -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; } } diff --git a/templates/base.html b/templates/base.html index 89a22a1..3479b4b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -9,6 +9,7 @@ + @@ -47,5 +49,18 @@ CODE / CIRCUITS / CONTINUITY HELSINKI, FI + diff --git a/themes/demo.html b/themes/demo.html new file mode 100644 index 0000000..41d1839 --- /dev/null +++ b/themes/demo.html @@ -0,0 +1,77 @@ + + + + + + Eternity Theme — Starter Template + + + + + + + + + +
+
+ /// STARTER TEMPLATE +

Reusable post-apocalyptic theme.

+

Drop eternity-theme.css into any site, wire up the toggle script, and switch between the daylight and wasteland palettes with a single data-theme attribute.

+ +
+ +
+
+

Card one

Short description of a feature or article.

+

Card two

Short description of a feature or article.

+

Card three

Short description of a feature or article.

+
+
+ +
+
+ + + +
+
This is a success message.
+
This is an error message.
+
+ +
+
+
example.jscopy
+
console.log("themed code block");
+
+
+
+ + + + + + diff --git a/themes/eternity-theme.css b/themes/eternity-theme.css new file mode 100644 index 0000000..707b494 --- /dev/null +++ b/themes/eternity-theme.css @@ -0,0 +1,107 @@ +/* Eternity Project theme — portable core styles. + Usage: 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; } +}