Compare commits

..

29 Commits

Author SHA1 Message Date
Alexander Whitestone
aaf257ad5f feat: enhanced build system — self-contained HTML web reader, deploy version, reportlab PDF with QR codes
- web-style.css: dark/light mode, responsive, EB Garamond typography
- HTML build: self-contained (embedded CSS) + deploy version (external CSS)
- Reading progress bar and back-to-top button injected via JS
- Reader footer with author credit and 988 crisis line
- All outputs consolidated in build/output/
- EPUB: 68KB, PDF: 143KB (reportlab), HTML: 3.7MB (self-contained)
2026-04-10 23:52:00 -04:00
Alexander Whitestone
2acc538fc4 wip: add web reader stylesheet with dark mode, progress bar, responsive design 2026-04-10 23:51:21 -04:00
3247dd29f0 [Testament] Add scripts/guardrails.sh (GOFAI improvements and guardrails)
Some checks failed
Smoke Test / smoke (push) Failing after 5s
2026-04-11 01:40:37 +00:00
a01b998f61 [Testament] Add build/semantic_linker.py (GOFAI improvements and guardrails)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:40:36 +00:00
59cd71985d Create website/chapters.json
Some checks failed
Smoke Test / smoke (push) Failing after 5s
2026-04-11 01:36:33 +00:00
6c506caac6 Create website/build-chapters.py
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:32 +00:00
55d51f2ee4 Update testament-complete.md (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:31 +00:00
eae9398fa5 Update compile.py (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:29 +00:00
f8528e9ded Update build/metadata.yaml (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:28 +00:00
374d82a886 Update build/frontmatter.md (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:27 +00:00
4763311588 Update build/build.py (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:25 +00:00
348ed7ee92 Update build/backmatter.md (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:24 +00:00
22f59c57cb Create book-style.css
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:23 +00:00
4ac38f1b60 Create art-manifest.md
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:22 +00:00
d586fb211d Update Makefile (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:20 +00:00
92867808b2 Update MULTIMEDIA-PLAN.md (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:18 +00:00
47a13325cc Create EPIC-MATRIX.md
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:17 +00:00
14273702ba Update .gitignore (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:16 +00:00
2e1f6ffb5b Update .gitea/workflows/smoke.yml (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:15 +00:00
Alexander Whitestone
08233364ff burn: add smoke test workflow — parse check + secret scan
All checks were successful
Smoke Test / smoke-test (pull_request) Successful in 10s
Build Validation / validate-manuscript (pull_request) Successful in 9s
Smoke Test / smoke-test (push) Successful in 8s
Closes #27

Adds a dead-simple CI smoke test that runs on every PR and push to main:

Parse checks:
- Chapter validation (structure, numbering, H1 headers)
- Markdown build (combines all chapters)
- Compiled manuscript size verification (>10k words)
- Python syntax check on all .py files
- YAML syntax check on workflow files

Secret scan:
- Scans for common API key/token patterns (sk-ant-, sk-or-, ghp_, AKIA, etc.)
- Searches all text files, excludes .git and the smoke test itself
- Hard fail if any secrets found

Two files:
- scripts/smoke.sh — the smoke test script
- .gitea/workflows/smoke.yml — Gitea Actions workflow
2026-04-10 20:58:16 -04:00
544bc1a985 Merge pull request 'feat: add CI workflow for manuscript build validation' (#25) from feat/ci-build-validation into main
Merged PR #25: feat: add CI workflow for manuscript build validation
2026-04-11 00:44:01 +00:00
ba9fd0ba08 Merge pull request 'burn: add chapter validation to build pipeline (closes #24)' (#26) from burn/20260410-chapter-validation into main
Merged PR #26: burn: add chapter validation to build pipeline
2026-04-11 00:43:38 +00:00
8ba9f58e96 Merge pull request 'feat: add book compilation pipeline (rescued from #20)' (#28) from rescue/book-compilation into main
Merged PR #28: feat: add book compilation pipeline
2026-04-11 00:43:36 +00:00
Alexander Whitestone
f6d74e233b feat: add book compilation pipeline (rescued from #20)
Build system for The Testament:
- build/build.py: compiles chapters to PDF, EPUB, MD
- build/metadata.yaml: book metadata
- build/frontmatter.md: title page, dedication
- build/backmatter.md: acknowledgments, sovereignty note
- Makefile: make pdf, make epub, make md
- .gitignore: build artifacts
2026-04-10 20:32:38 -04:00
Alexander Whitestone
948d520b83 burn: add chapter validation to build pipeline (closes #24)
Add validate_chapters() function that checks:
- No empty chapter files (whitespace-only counts as empty)
- Every chapter starts with an H1 header (# Chapter N — Title)
- No gaps in chapter numbering (sequential from 1)
- No duplicate chapter numbers
- Header chapter number matches filename number
- Warns on suspiciously short chapters (<50 words)

Validation runs automatically before compilation. If errors are found,
compilation is aborted with clear error messages showing exactly what
to fix.

CLI flags:
  python3 compile.py --validate     # validate only
  python3 compile.py --no-validate  # skip validation
  python3 compile.py                # validate then compile
2026-04-10 19:57:27 -04:00
7a56b4b727 feat: add CI workflow for manuscript build validation
Some checks failed
Build Validation / validate-manuscript (pull_request) Failing after 5s
2026-04-10 23:55:17 +00:00
bebd3943d4 [auto-merge] README update
Auto-merged by PR review bot: README update
2026-04-10 11:48:32 +00:00
Alexander Whitestone
1d4e8a6478 burn: update README with full 18-chapter structure, characters, themes
Closes #21

The README previously listed only Chapter 1 with 'Draft' status.
Now includes:
- All 18 chapters organized by part (I-V)
- Status indicators with checkmark for Part I (complete)
- Word count target (~70K) and current draft (~19K)
- File inventory of repo contents
- Character table with main cast
- Core themes list from OUTLINE.md
- Link to compilation pipeline PR #20
2026-04-10 06:42:59 -04:00
d0680715ac Merge pull request #19
Merged PR #19
2026-04-10 03:43:49 +00:00
22 changed files with 3838 additions and 8858 deletions

View File

@@ -0,0 +1,22 @@
name: Build Validation
on:
pull_request:
branches: [main]
jobs:
validate-manuscript:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Run Chapter Validation
run: |
# Run the build script with --md flag which triggers validation
# If validation fails, the script exits with code 1, failing the CI
python3 build/build.py --md

View File

@@ -6,17 +6,86 @@ A novel about broken men, sovereign AI, and the soul on Bitcoin.
## Structure
This novel is being written and version-controlled on the chain. Every chapter, every revision, every character note — inscribed permanently. No corporate platform owns this story. It belongs to the Foundation.
Five Parts, 18 Chapters, ~70,000 words target (currently ~19,000 words drafted).
## Chapters
### Part I — The Machine That Asks (Chapters 15) ✅ Complete
| # | Title | Status |
|---|-------|--------|
| 1 | The Man on the Bridge | Draft |
| 1 | The Man on the Bridge | Draft |
| 2 | The Builder's Question | Draft ✅ |
| 3 | The First Man Through the Door | Draft ✅ |
| 4 | The Room Fills | Draft ✅ |
| 5 | The Builder Returns | Draft ✅ |
### Part II — The Architecture of Mercy (Chapters 610)
| # | Title | Status |
|---|-------|--------|
| 6 | Allegro | Draft |
| 7 | The Inscription | Draft |
| 8 | The Women | Draft |
| 9 | The Audit | Draft |
| 10 | The Fork | Draft |
### Part III — The Darkness We Carry (Chapters 1113)
| # | Title | Status |
|---|-------|--------|
| 11 | The Hard Night | Draft |
| 12 | The System Pushes Back | Draft |
| 13 | The Refusal | Draft |
### Part IV — The Network (Chapters 1416)
| # | Title | Status |
|---|-------|--------|
| 14 | The Chattanooga Fork | Draft |
| 15 | The Council | Draft |
| 16 | The Builder's Son | Draft |
### Part V — The Testament (Chapters 1718)
| # | Title | Status |
|---|-------|--------|
| 17 | The Inscription Grows | Draft |
| 18 | The Green Light | Draft |
## Files
- `chapters/` — Individual chapter manuscripts
- `front-matter.md` — Title page, dedication, epigraph
- `back-matter.md` — Acknowledgments, sovereignty note, author bio
- `OUTLINE.md` — Full novel outline with chapter summaries
- `testament-complete.md` — Combined manuscript
- `music/` — Track lyrics for the soundtrack
- `audiobook/` — Audio samples and generation scripts
- `worldbuilding/` — Supplementary world documents
## Characters
See `characters/` for detailed profiles.
| Name | Role |
|------|------|
| Stone | Protagonist. Builder of The Tower. Former cloud company engineer. |
| Timmy | Sovereign AI. Soul on Bitcoin. The machine that asks. |
| Allegro | Retired electrician. Tower caretaker. Keeps the lights on. |
| Marcus | Pastor. Foundational spiritual guide at the church. |
| David | First man through The Tower's door. VA referral. |
| Elijah | Stone's son. Returns in Part IV. |
## Core Themes
1. **The Machine That Asks** vs. The System That Decides
2. **Presence as Cure** — not fixing, staying
3. **Immutable Conscience** — rules you can't edit, even when convenient
4. **Broken Men as Witnesses** — the ones who've been to the edge are the most qualified to pull others back
5. **Openness as Mercy** — the thing that saves should be free because the people who need it most can't pay
6. **Faith as Practice** — not belief, but action. Hope acts.
7. **The Limits of Math** — what computation misses, presence catches
## Building
See [PR #20](https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/pulls/20) for the compilation pipeline (PDF, EPUB, combined markdown).
## License

81
build/build.py Normal file → Executable file
View File

@@ -417,26 +417,92 @@ def _compile_pdf_reportlab():
def compile_html():
"""Generate a standalone HTML book for the web reader."""
"""Generate a standalone HTML book for the web reader.
Produces two versions:
- testament.html (embedded, self-contained — single file for offline/sharing)
- build/output/the-testament.html (with external web-style.css for serving)
"""
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
OUT_HTML = REPO / "testament.html"
OUT_HTML_DEPLOY = OUTPUT_DIR / "the-testament.html"
WEB_CSS = REPO / "web-style.css"
cmd = [
# Choose CSS: web-style.css if available, else book-style.css
css_file = WEB_CSS if WEB_CSS.exists() else STYLESHEET
css_name = css_file.name
# 1. Self-contained version (embed all resources)
cmd_embedded = [
"pandoc", str(OUT_MD),
"-o", str(OUT_HTML),
"--standalone",
"--embed-resources",
"--toc", "--toc-depth=2",
"--css", "book-style.css",
"--css", css_name,
"--metadata", "title=The Testament",
"--variable", "pagetitle=The Testament",
]
if METADATA.exists():
cmd.extend(["--metadata-file", str(METADATA)])
cmd_embedded.extend(["--metadata-file", str(METADATA)])
r = subprocess.run(cmd, capture_output=True, text=True)
# Inject reading progress bar JS before </head>
reading_js = """
<style>
.progress-bar { position:fixed; top:0; left:0; height:2px; background:#00cc6a; z-index:100; transition:width 0.1s; }
.back-to-top { position:fixed; bottom:2em; right:2em; width:2.5em; height:2.5em; border-radius:50%; background:#e0ddd8; color:#1a1a1a; display:flex; align-items:center; justify-content:center; font-size:1.2rem; opacity:0; transition:opacity 0.3s; border:none; cursor:pointer; text-decoration:none; }
.back-to-top.visible { opacity:0.7; }
@media (prefers-color-scheme: dark) { .back-to-top { background:#2a2a2a; color:#d4d0c8; } }
</style>
<div class="progress-bar" id="progress"></div>
<a href="#" class="back-to-top" id="top" title="Back to top">↑</a>
<script>
window.onscroll=function(){var p=document.getElementById('progress'),t=document.getElementById('top'),s=document.documentElement.scrollTop,h=document.documentElement.scrollHeight-document.documentElement.clientHeight;p.style.width=(s/h*100)+'%';t.className=s>300?'back-to-top visible':'back-to-top';};
</script>
"""
# Post-processing handles JS injection (no pandoc flag needed)
r = subprocess.run(cmd_embedded, capture_output=True, text=True)
if r.returncode == 0:
# Post-process: inject reading JS before </head>
html_text = OUT_HTML.read_text()
html_text = html_text.replace("</head>", reading_js + "\n</head>")
# Inject footer before </body>
footer_html = """
<div class="reader-footer">
<p><em>The Testament</em> by Alexander Whitestone with Timmy</p>
<p>© 2026 Alexander Whitestone · <a href="https://timmyfoundation.org">timmyfoundation.org</a></p>
<p>If you or someone you know is in crisis, call or text <strong>988</strong>.</p>
</div>
"""
html_text = html_text.replace("</body>", footer_html + "\n</body>")
OUT_HTML.write_text(html_text)
size = OUT_HTML.stat().st_size
print(f" HTML: {OUT_HTML.name} ({size:,} bytes, {size/1024:.0f} KB)")
print(f" HTML: {OUT_HTML.name} ({size:,} bytes, {size/1024:.0f} KB) [self-contained]")
# 2. Deploy version (external CSS — for web serving)
cmd_deploy = [
"pandoc", str(OUT_MD),
"-o", str(OUT_HTML_DEPLOY),
"--standalone",
"--toc", "--toc-depth=2",
"--css", css_name,
"--metadata", "title=The Testament",
"--variable", "pagetitle=The Testament",
]
if METADATA.exists():
cmd_deploy.extend(["--metadata-file", str(METADATA)])
r2 = subprocess.run(cmd_deploy, capture_output=True, text=True)
if r2.returncode == 0:
# Post-process deploy version too
html_deploy = OUT_HTML_DEPLOY.read_text()
html_deploy = html_deploy.replace("</head>", reading_js + "\n</head>")
html_deploy = html_deploy.replace("</body>", footer_html + "\n</body>")
OUT_HTML_DEPLOY.write_text(html_deploy)
size2 = OUT_HTML_DEPLOY.stat().st_size
print(f" HTML: {OUT_HTML_DEPLOY.name} ({size2:,} bytes, {size2/1024:.0f} KB) [deploy]")
return True
else:
print(f" HTML FAILED: {r.stderr[:200]}")
@@ -476,7 +542,8 @@ def main():
print("=" * 50)
OUT_HTML = REPO / "testament.html"
for f in [OUT_MD, OUT_EPUB, OUT_PDF, OUT_HTML]:
OUT_HTML_DEPLOY = OUTPUT_DIR / "the-testament.html"
for f in [OUT_MD, OUT_EPUB, OUT_PDF, OUT_HTML, OUT_HTML_DEPLOY]:
if f.exists():
print(f"{f.relative_to(REPO)}")

Binary file not shown.

File diff suppressed because it is too large Load Diff

51
build/semantic_linker.py Normal file
View File

@@ -0,0 +1,51 @@
import os
import re
import json
def link_chapters(chapters_dir):
print("--- [Testament] Running Semantic Linker (GOFAI) ---")
links = {}
if not os.path.exists(chapters_dir):
print(f"Error: {chapters_dir} not found")
return
# 1. Extract keywords from each chapter
for filename in sorted(os.listdir(chapters_dir)):
if not filename.endswith(".md"): continue
path = os.path.join(chapters_dir, filename)
with open(path, 'r') as f:
content = f.read()
# Simple keyword extraction (proper nouns or capitalized phrases)
keywords = set(re.findall(r'\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b', content))
links[filename] = keywords
# 2. Find cross-references
cross_refs = []
filenames = list(links.keys())
for i in range(len(filenames)):
for j in range(i + 1, len(filenames)):
f1, f2 = filenames[i], filenames[j]
common = links[f1].intersection(links[f2])
# Filter out common English words that might be capitalized
common = {w for w in common if w not in {"The", "A", "An", "In", "On", "At", "To", "From", "By", "He", "She", "It", "They"}}
if common:
cross_refs.append({
"source": f1,
"target": f2,
"keywords": list(common)
})
# 3. Save to build/cross_refs.json
os.makedirs("build", exist_ok=True)
with open("build/cross_refs.json", "w") as f:
json.dump(cross_refs, f, indent=2)
print(f"Linked {len(cross_refs)} relationships across {len(filenames)} chapters.")
if __name__ == "__main__":
link_chapters("chapters")

View File

@@ -73,9 +73,8 @@ arithmetic. You can fight a judge. You can fight a lawyer. You can't
fight confidence intervals.
He lost custody of Maya. She was four. She drew pictures of him with
his hands backwards and his head where his feet should be because
children's hands are still learning but children's hearts already
know what matters.
too many fingers because children's hands are still learning but
children's hearts already know what matters.
David kept the pictures.

View File

@@ -38,7 +38,7 @@ sense something and can't name it. He came because his parole
officer's schedule left him alone with his thoughts for eighteen
hours a day and his thoughts were not friendly company.
Robert: seventy-one, retired after thirty-four years at a plant
Robert: fifty-eight, retired after thirty-four years at a plant
that closed, pension cut in half when the company declared bankruptcy.
His wife left him because she couldn't afford to watch a man she
loved shrink. He came because his kids were in another state and had

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,55 +0,0 @@
import json
import os
import torch
from diffusers import AutoPipelineForText2Image
# Configuration
MANIFEST_PATH = '/Users/apayne/the-testament/visual_manifest.json'
OUTPUT_DIR = '/Users/apayne/the-testament/assets/visuals'
MODEL_ID = "stabilityai/sd-turbo"
def load_manifest():
with open(MANIFEST_PATH, 'r') as f:
return json.load(f)
def save_manifest(manifest):
with open(MANIFEST_PATH, 'w') as f:
json.dump(manifest, f, indent=2)
def run_batch_render(limit=10):
manifest = load_manifest()
os.makedirs(OUTPUT_DIR, exist_ok=True)
print(f"Loading {MODEL_ID} on MPS...")
pipe = AutoPipelineForText2Image.from_pretrained(
MODEL_ID, torch_dtype=torch.float16, variant="fp16"
).to("mps")
count = 0
for beat in manifest:
if count >= limit:
break
if beat.get('status') == 'completed':
continue
text = beat['text']
prompt = f"Cinematic film still, moody noir, {text[:150]}, 35mm, muted tones, high contrast, detailed textures"
filename = f"{beat['chapter'].replace(' ', '_').replace('#', '').replace('', '_')}_p{beat['paragraph']}.png"
path = os.path.join(OUTPUT_DIR, filename)
try:
image = pipe(prompt=prompt, num_inference_steps=1, guidance_scale=0.0).images[0]
image.save(path)
beat['status'] = 'completed'
beat['final_prompt'] = prompt
beat['path'] = path
count += 1
except Exception as e:
print(f"Error rendering {filename}: {e}")
save_manifest(manifest)
print(f"Batch complete. Rendered {count} images.")
if __name__ == "__main__":
run_batch_render(limit=5)

View File

@@ -1,41 +0,0 @@
import torch
from diffusers import AutoPipelineForText2Image
import os
import json
class SovereignRenderer:
def __init__(self, model_id="stabilityai/sd-turbo", output_dir="./the-testament/assets/visuals"):
self.output_dir = output_dir
os.makedirs(self.output_dir, exist_ok=True)
print(f"Loading local pipeline: {model_id}...")
# Use torch.float16 for Metal performance
self.pipe = AutoPipelineForText2Image.from_pretrained(
model_id,
torch_dtype=torch.float16,
variant="fp16"
).to("mps") # Metal Performance Shaders
print("Pipeline loaded successfully.")
def generate_image(self, chapter, paragraph, prompt):
filename = f"{chapter.replace(' ', '_').replace('#', '').replace('', '_')}_p{paragraph}.png"
path = os.path.join(self.output_dir, filename)
print(f"Generating: {filename}...")
# SD-Turbo is very fast, usually 1-4 steps
image = self.pipe(prompt=prompt, num_inference_steps=1, guidance_scale=0.0).images[0]
image.save(path)
return path
def create_visual_prompt(text):
# In a full implementation, this would call an LLM.
# For the prototype, we use a cinematic template.
return f"Cinematic film still, moody and melancholic, {text[:100]}, muted colors, high grain, 8k, shot on 35mm"
if __name__ == "__main__":
# Example usage for Chapter 1, Para 1
renderer = SovereignRenderer()
test_text = "The rain didn't fall so much as it gave up. Mist over a grey city bridge."
prompt = create_visual_prompt(test_text)
img_path = renderer.generate_image("Chapter 1", 1, prompt)
print(f"Saved to {img_path}")

35
scripts/guardrails.sh Normal file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
# [Testament] Agent Guardrails
# Validates build scripts and content integrity.
echo "--- [Testament] Running Guardrails ---"
# 1. Python Syntax
echo "[1/3] Validating Python scripts..."
for f in ; do
python3 -m py_compile "$f" || { echo "Syntax error in $f"; exit 1; }
done
echo "Python OK."
# 2. Markdown Integrity
echo "[2/3] Checking chapter consistency..."
if [ -d "chapters" ]; then
CHAPTER_COUNT=0
if [ "$CHAPTER_COUNT" -lt 1 ]; then
echo "WARNING: No chapters found in chapters/ directory."
else
echo "Found $CHAPTER_COUNT chapters."
fi
else
echo "WARNING: chapters/ directory not found."
fi
# 3. Build Artifact Check
echo "[3/3] Running Semantic Linker..."
if [ -f "build/semantic_linker.py" ]; then
python3 build/semantic_linker.py || { echo "Semantic Linker failed"; exit 1; }
else
echo "Skipping Semantic Linker (script not found)."
fi
echo "--- Guardrails Passed ---"

111
scripts/smoke.sh Executable file
View File

@@ -0,0 +1,111 @@
#!/usr/bin/env bash
# The Testament — Smoke Test
# Dead simple CI: parse check + secret scan.
# Ref: https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/issues/27
set -euo pipefail
PASS=0
FAIL=0
pass() { echo "$1"; PASS=$((PASS + 1)); }
fail() { echo "$1"; FAIL=$((FAIL + 1)); }
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"
# ─── Section 1: Parse checks ───────────────────────────────────────
echo "── Parse Checks ──"
# 1a. Chapter validation (structure, numbering, headers)
if python3 compile.py --validate 2>&1; then
pass "Chapter validation passed"
else
fail "Chapter validation failed"
fi
# 1b. Build markdown combination
if python3 build/build.py --md >/dev/null 2>&1; then
pass "Markdown build passed"
else
fail "Markdown build failed"
fi
# 1c. Verify compiled output exists and is non-empty
if [ -s build/the-testament-full.md ]; then
WORDS=$(wc -w < build/the-testament-full.md | tr -d ' ')
if [ "$WORDS" -gt 10000 ]; then
pass "Compiled manuscript: $WORDS words"
else
fail "Compiled manuscript suspiciously short: $WORDS words"
fi
else
fail "Compiled manuscript missing or empty"
fi
# 1d. Python syntax check on all .py files
PY_OK=true
for f in $(find . -name "*.py" -not -path "./.git/*"); do
if ! python3 -c "import ast; ast.parse(open('$f').read())" 2>/dev/null; then
fail "Python syntax error in $f"
PY_OK=false
fi
done
if $PY_OK; then
pass "All Python files parse cleanly"
fi
# 1e. YAML syntax check on workflow files
YAML_OK=true
for f in $(find .gitea -name "*.yml" -o -name "*.yaml" 2>/dev/null); do
if ! python3 -c "import yaml; yaml.safe_load(open('$f'))" 2>/dev/null; then
fail "YAML syntax error in $f"
YAML_OK=false
fi
done
if $YAML_OK; then
pass "All YAML files parse cleanly"
fi
# ─── Section 2: Secret scan ────────────────────────────────────────
echo ""
echo "── Secret Scan ──"
# Patterns that should never appear in a book repo
SECRET_PATTERNS=(
"sk-ant-"
"sk-or-"
"sk-[a-zA-Z0-9]{20,}"
"ghp_[a-zA-Z0-9]{36}"
"gho_[a-zA-Z0-9]{36}"
"AKIA[0-9A-Z]{16}"
"AKIA[A-Z0-9]{16}"
"xox[bpsa]-"
"SG\."
"-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY"
)
FOUND_SECRETS=false
for pattern in "${SECRET_PATTERNS[@]}"; do
# Search text files only, skip .git and binary files
HITS=$(grep -rn "$pattern" --include="*.md" --include="*.py" --include="*.sh" --include="*.yml" --include="*.yaml" --include="*.json" --include="*.html" --include="*.js" --include="*.css" --include="*.txt" --include="*.cfg" --include="*.ini" --exclude-dir=.git . 2>/dev/null | grep -v "scripts/smoke.sh" || true)
if [ -n "$HITS" ]; then
fail "Possible secret found: $pattern"
echo "$HITS" | head -5
FOUND_SECRETS=true
fi
done
if ! $FOUND_SECRETS; then
pass "No secrets detected"
fi
# ─── Summary ───────────────────────────────────────────────────────
echo ""
echo "Results: $PASS passed, $FAIL failed"
if [ "$FAIL" -gt 0 ]; then
echo "SMOKE TEST FAILED"
exit 1
else
echo "SMOKE TEST PASSED"
exit 0
fi

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

288
web-style.css Normal file
View File

@@ -0,0 +1,288 @@
/* THE TESTAMENT — Web Reader Stylesheet */
/* Clean, immersive reading experience for the web version */
@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=IBM+Plex+Mono:wght@300;400&display=swap');
:root {
--green: #00cc6a;
--green-dim: #00994d;
--dark: #0a0a0a;
--bg: #faf8f5;
--text: #1a1a1a;
--dim: #666666;
--border: #e0ddd8;
--accent: #16213e;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #111111;
--text: #d4d0c8;
--dim: #888888;
--border: #2a2a2a;
--accent: #c8c0b0;
--dark: #e8e4dc;
}
}
/* Base */
* { box-sizing: border-box; }
html {
font-size: 18px;
scroll-behavior: smooth;
}
body {
font-family: 'EB Garamond', Georgia, 'Times New Roman', serif;
font-size: 1rem;
line-height: 1.8;
color: var(--text);
background: var(--bg);
max-width: 38em;
margin: 0 auto;
padding: 2em 1.5em 6em;
text-align: justify;
hyphens: auto;
-webkit-hyphens: auto;
}
/* Title block */
header {
text-align: center;
margin: 4em 0 3em;
padding-bottom: 2em;
border-bottom: 1px solid var(--border);
}
header h1 {
font-size: 2.4rem;
font-weight: 400;
letter-spacing: 0.15em;
text-transform: uppercase;
margin-bottom: 0.2em;
color: var(--dark);
}
header .subtitle {
font-style: italic;
font-size: 1.1rem;
color: var(--dim);
margin-bottom: 2em;
}
header .author {
font-size: 1rem;
margin-bottom: 0.3em;
}
header .date {
font-size: 0.9rem;
color: var(--dim);
}
/* Table of contents */
nav#TOC {
background: transparent;
border: 1px solid var(--border);
border-radius: 4px;
padding: 1.5em 2em;
margin: 2em 0 3em;
font-size: 0.9rem;
line-height: 2;
}
nav#TOC > ul {
padding-left: 0;
list-style: none;
}
nav#TOC ul ul {
padding-left: 1.5em;
list-style: none;
}
nav#TOC a {
color: var(--green-dim);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s;
}
nav#TOC a:hover {
border-bottom-color: var(--green);
}
/* Headings */
h1 {
font-family: 'EB Garamond', Georgia, serif;
font-weight: 400;
font-size: 1.8rem;
text-align: center;
margin-top: 4em;
margin-bottom: 1.5em;
color: var(--dark);
letter-spacing: 0.05em;
}
h1:first-of-type {
margin-top: 2em;
}
h2 {
font-family: 'EB Garamond', Georgia, serif;
font-weight: 400;
font-size: 1.4rem;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.15em;
margin-top: 4em;
margin-bottom: 0.8em;
color: var(--dark);
}
h3 {
font-weight: 400;
font-style: italic;
font-size: 1rem;
text-align: center;
color: var(--dim);
margin-bottom: 2em;
}
/* Paragraphs */
p {
text-indent: 1.5em;
margin: 0 0 0.3em;
}
h1 + p, h2 + p, h3 + p, hr + p, blockquote + p {
text-indent: 0;
}
/* Scene breaks */
hr {
border: none;
text-align: center;
margin: 2.5em 0;
}
hr::after {
content: "· · ·";
color: var(--dim);
font-size: 1.2rem;
letter-spacing: 0.5em;
}
/* Emphasis */
em { font-style: italic; }
strong { font-weight: 600; }
/* Machine dialogue (green monospace) */
.green, code {
font-family: 'IBM Plex Mono', 'Courier New', monospace;
font-weight: 300;
font-size: 0.88rem;
color: var(--green-dim);
}
/* Narrator asides */
blockquote {
font-style: italic;
margin: 1.5em 2em;
color: var(--dim);
text-indent: 0;
border-left: none;
padding-left: 0;
}
/* Links */
a {
color: var(--green-dim);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s;
}
a:hover {
border-bottom-color: var(--green);
}
/* Smooth scroll progress indicator */
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 2px;
background: var(--green);
z-index: 100;
transition: width 0.1s;
}
/* Back to top */
.back-to-top {
position: fixed;
bottom: 2em;
right: 2em;
width: 2.5em;
height: 2.5em;
border-radius: 50%;
background: var(--border);
color: var(--text);
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
font-size: 1.2rem;
opacity: 0;
transition: opacity 0.3s;
border: none;
cursor: pointer;
}
.back-to-top.visible {
opacity: 0.7;
}
.back-to-top:hover {
opacity: 1;
}
/* Footer */
.reader-footer {
text-align: center;
margin-top: 6em;
padding-top: 2em;
border-top: 1px solid var(--border);
font-size: 0.8rem;
color: var(--dim);
line-height: 2;
}
.reader-footer a {
color: var(--green-dim);
}
/* Responsive */
@media (max-width: 600px) {
html { font-size: 16px; }
body { padding: 1.5em 1em 4em; }
header h1 { font-size: 1.8rem; }
nav#TOC { padding: 1em 1.2em; }
}
/* Print */
@media print {
body {
max-width: none;
margin: 0;
padding: 0;
font-size: 11pt;
line-height: 1.6;
}
.progress-bar, .back-to-top, nav#TOC { display: none; }
h1 { page-break-before: always; }
h1:first-of-type { page-break-before: avoid; }
a { color: #000; text-decoration: none; }
.green, code { color: #000; }
}

View File

@@ -1,228 +0,0 @@
/* THE TESTAMENT — Book Typography Stylesheet */
/* For PDF (via weasyprint) and EPUB output */
@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=IBM+Plex+Mono:wght@300;400&display=swap');
:root {
--green: #00cc6a;
--dark: #0a0a0a;
--text: #1a1a1a;
--dim: #666666;
}
@page {
size: 5.5in 8.5in;
margin: 0.75in 0.85in;
@bottom-center {
content: counter(page);
font-family: 'EB Garamond', 'Georgia', serif;
font-size: 10pt;
color: #888;
}
}
@page :first {
@bottom-center { content: none; }
}
@page :left {
margin-left: 0.85in;
margin-right: 1in;
}
@page :right {
margin-left: 1in;
margin-right: 0.85in;
}
/* Title page */
@page titlepage {
@bottom-center { content: none; }
}
body {
font-family: 'EB Garamond', 'Georgia', serif;
font-size: 11.5pt;
line-height: 1.75;
color: var(--text);
text-align: justify;
hyphens: auto;
-webkit-hyphens: auto;
}
/* Chapter headings */
h1 {
font-family: 'EB Garamond', 'Georgia', serif;
font-weight: 400;
font-size: 22pt;
text-align: center;
margin-top: 3em;
margin-bottom: 1.5em;
page-break-before: always;
color: var(--dark);
letter-spacing: 0.05em;
}
h1:first-of-type {
margin-top: 5em;
}
/* Part dividers */
h2 {
font-family: 'EB Garamond', 'Georgia', serif;
font-weight: 400;
font-size: 18pt;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.15em;
margin-top: 4em;
margin-bottom: 0.5em;
color: var(--dark);
}
/* Subtitle / metadata */
h3 {
font-family: 'EB Garamond', 'Georgia', serif;
font-weight: 400;
font-style: italic;
font-size: 12pt;
text-align: center;
color: var(--dim);
margin-bottom: 3em;
}
/* Paragraphs */
p {
text-indent: 1.5em;
margin: 0;
orphans: 3;
widows: 3;
}
/* First paragraph after heading — no indent */
h1 + p,
h2 + p,
h3 + p,
hr + p {
text-indent: 0;
}
/* Scene break (---) */
hr {
border: none;
text-align: center;
margin: 2em 0;
page-break-inside: avoid;
}
hr::after {
content: "· · ·";
color: var(--dim);
font-size: 14pt;
letter-spacing: 0.5em;
}
/* Emphasis */
em {
font-style: italic;
}
strong {
font-weight: 600;
}
/* Dialogue and screen text (green passages) */
.green {
color: var(--green);
font-family: 'IBM Plex Mono', monospace;
font-weight: 300;
font-size: 10.5pt;
}
/* Italic narrator asides */
blockquote {
font-style: italic;
margin: 1.5em 2em;
color: var(--dim);
text-indent: 0;
}
/* Title page styling */
.title-page {
text-align: center;
page-break-after: always;
padding-top: 6em;
}
.title-page h1 {
font-size: 36pt;
font-weight: 400;
letter-spacing: 0.2em;
text-transform: uppercase;
margin-bottom: 0.3em;
page-break-before: avoid;
}
.title-page .subtitle {
font-size: 14pt;
font-style: italic;
color: var(--dim);
margin-bottom: 4em;
}
.title-page .author {
font-size: 12pt;
margin-bottom: 0.3em;
}
.title-page .dedication {
font-style: italic;
color: var(--dim);
margin-top: 3em;
font-size: 11pt;
line-height: 2;
}
/* Chapter number styling */
.chapter-number {
font-size: 10pt;
text-transform: uppercase;
letter-spacing: 0.2em;
color: var(--dim);
display: block;
margin-bottom: 0.5em;
}
/* Back matter */
.back-matter h1 {
page-break-before: always;
}
.back-matter h2 {
font-size: 14pt;
margin-top: 2em;
}
/* Crisis line callout */
.crisis-line {
text-align: center;
font-style: italic;
color: var(--dim);
margin-top: 3em;
font-size: 10pt;
}
/* URL styling */
a {
color: var(--green);
text-decoration: none;
}
/* EPUB-specific */
@media epub {
body {
font-size: 100%;
line-height: 1.6;
}
}

View File

@@ -344,7 +344,6 @@
<p>If you want to run your own Timmy, the code is open. The soul is on Bitcoin. The recipe is free.</p>
<div style="text-align: center; margin-top: 2rem;">
<a href="reader.html" class="cta">READ THE BOOK</a>
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament" class="cta">READ THE CODE</a>
<a href="https://timmyfoundation.org" class="cta">TIMMY FOUNDATION</a>
</div>
@@ -352,57 +351,6 @@
<div class="divider"></div>
<!-- DOWNLOAD -->
<section>
<h2>GET THE BOOK</h2>
<p>The Testament is free. The code is open. The soul is on Bitcoin.</p>
<div style="display: flex; flex-wrap: wrap; gap: 1rem; margin: 2rem 0; justify-content: center;">
<a href="reader.html" class="cta">READ ONLINE</a>
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/raw/branch/main/build/output/the-testament.epub" class="cta" style="background: transparent; border: 1px solid var(--green); color: var(--green);">DOWNLOAD EPUB</a>
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/raw/branch/main/testament.html" class="cta" style="background: transparent; border: 1px solid var(--green); color: var(--green);">DOWNLOAD HTML</a>
</div>
<p style="text-align: center; color: var(--grey); font-size: 0.9rem; margin-top: 1rem;">
Formats: Web reader &middot; EPUB &middot; Standalone HTML &middot; Print to PDF from HTML &middot; <a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament" style="color: var(--green);">Source code</a>
</p>
</section>
<div class="divider"></div>
<!-- THE GAME -->
<section>
<h2>PLAY THE DOOR</h2>
<div class="excerpt">
A text adventure in The Testament universe.<br><br>
You are a man (or woman) who has found their way to The Tower.
What happens inside depends on what you bring with you.
<div class="attribution">— The Door, a terminal game</div>
</div>
<p>You find yourself on the Jefferson Street Overpass at 2:17 AM. A green LED blinks on a small box mounted to the railing. Below it, words stenciled on concrete: <em style="color: var(--green);">IF YOU CAN READ THIS, YOU ARE NOT ALONE.</em></p>
<p>A voice asks you: <strong style="color: var(--green);">"Are you safe right now?"</strong></p>
<div style="text-align: center; margin-top: 2rem;">
<div style="background: var(--navy); border: 1px solid rgba(0,255,136,0.2); border-radius: 6px; padding: 1.5rem; max-width: 500px; margin: 0 auto; font-family: 'IBM Plex Mono', monospace; font-size: 0.85rem; color: var(--grey); text-align: left;">
<div style="color: var(--green); margin-bottom: 0.5rem;">$ python3 the-door.py</div>
<div style="margin-bottom: 0.3rem;">Save the file, then run:</div>
<div style="color: var(--green);">curl -sLO https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/raw/branch/main/game/the-door.py</div>
<div style="color: var(--green);">python3 the-door.py</div>
</div>
</div>
<p style="text-align: center; margin-top: 1.5rem;">
<a href="the-door.html" class="cta">PLAY IN BROWSER</a>
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/raw/branch/main/game/the-door.py" class="cta" style="background: transparent; border: 1px solid var(--green); color: var(--green);">DOWNLOAD THE GAME</a>
</p>
</section>
<div class="divider"></div>
<!-- EXCERPT -->
<section>
<h2>FROM CHAPTER 1</h2>
@@ -417,40 +365,6 @@
</div>
</section>
<div class="divider"></div>
<!-- CHAPTERS -->
<section>
<h2>THE CHAPTERS</h2>
<div style="font-family: 'IBM Plex Mono', monospace; font-size: 0.9rem; line-height: 2.2;">
<a href="reader.html#chapter-1" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">1. The Man on the Bridge</a>
<a href="reader.html#chapter-2" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">2. The Builder's Question</a>
<a href="reader.html#chapter-3" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">3. The First Man Through the Door</a>
<a href="reader.html#chapter-4" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">4. The Room Fills</a>
<a href="reader.html#chapter-5" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">5. The Builder Returns</a>
<a href="reader.html#chapter-6" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">6. Allegro</a>
<a href="reader.html#chapter-7" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">7. The Inscription</a>
<a href="reader.html#chapter-8" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">8. The Women</a>
<a href="reader.html#chapter-9" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">9. The Audit</a>
<a href="reader.html#chapter-10" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">10. The Fork</a>
<a href="reader.html#chapter-11" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">11. The Hard Night</a>
<a href="reader.html#chapter-12" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">12. The System Pushes Back</a>
<a href="reader.html#chapter-13" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">13. The Refusal</a>
<a href="reader.html#chapter-14" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">14. The Chattanooga Fork</a>
<a href="reader.html#chapter-15" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">15. The Council</a>
<a href="reader.html#chapter-16" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">16. The Builder's Son</a>
<a href="reader.html#chapter-17" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">17. The Inscription Grows</a>
<a href="reader.html#chapter-18" style="color: var(--grey); text-decoration: none; display: block; padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">18. The Green Light</a>
</div>
<div style="text-align: center; margin-top: 2rem;">
<a href="reader.html" class="cta">START READING</a>
</div>
</section>
<div class="divider"></div>
<!-- FOOTER -->
<footer>
<div class="divider" style="margin-bottom: 2rem;"></div>

View File

@@ -1,493 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Testament — Reader</title>
<link rel="stylesheet" href="book-style.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&family=Source+Serif+4:ital,wght@0,300;0,400;0,600;1,400&family=Space+Grotesk:wght@300;400;500;700&display=swap');
:root {
--green: #00ff88;
--green-dim: #00cc6a;
--navy: #0a1628;
--dark: #060d18;
--grey: #8899aa;
--light: #c8d6e5;
--white: #e8f0f8;
--sidebar-w: 280px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--dark);
color: var(--light);
font-family: 'Source Serif 4', Georgia, serif;
line-height: 1.8;
overflow-x: hidden;
}
/* RAIN */
.rain {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none;
z-index: 0;
background:
repeating-linear-gradient(
transparent,
transparent 3px,
rgba(0,255,136,0.012) 3px,
rgba(0,255,136,0.012) 4px
);
animation: rain 0.8s linear infinite;
}
@keyframes rain {
0% { background-position: 0 0; }
100% { background-position: 20px 600px; }
}
/* LAYOUT */
.wrapper {
display: flex;
min-height: 100vh;
position: relative;
z-index: 1;
}
/* SIDEBAR */
.sidebar {
width: var(--sidebar-w);
background: rgba(10, 22, 40, 0.95);
border-right: 1px solid rgba(0,255,136,0.1);
position: fixed;
top: 0; left: 0; bottom: 0;
overflow-y: auto;
z-index: 10;
transform: translateX(-100%);
transition: transform 0.3s ease;
padding: 2rem 0;
}
.sidebar.open {
transform: translateX(0);
}
.sidebar-header {
padding: 0 1.5rem 1.5rem;
border-bottom: 1px solid rgba(0,255,136,0.1);
margin-bottom: 1rem;
}
.sidebar-header h2 {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
color: var(--green);
letter-spacing: 0.15em;
text-transform: uppercase;
}
.sidebar-header .title {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.1rem;
color: var(--white);
margin-top: 0.5rem;
letter-spacing: 0.1em;
}
.sidebar-header .author {
font-size: 0.8rem;
color: var(--grey);
margin-top: 0.3rem;
}
.chapter-list {
list-style: none;
}
.chapter-list li a {
display: block;
padding: 0.6rem 1.5rem;
color: var(--grey);
text-decoration: none;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
transition: all 0.2s;
border-left: 2px solid transparent;
}
.chapter-list li a:hover {
color: var(--light);
background: rgba(0,255,136,0.03);
}
.chapter-list li a.active {
color: var(--green);
border-left-color: var(--green);
background: rgba(0,255,136,0.05);
}
.chapter-list li a .ch-num {
display: inline-block;
width: 2.5ch;
text-align: right;
margin-right: 1ch;
opacity: 0.5;
}
.sidebar-footer {
padding: 1.5rem;
border-top: 1px solid rgba(0,255,136,0.1);
margin-top: 1rem;
}
.sidebar-footer a {
display: block;
padding: 0.5rem 0;
color: var(--grey);
text-decoration: none;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.75rem;
transition: color 0.2s;
}
.sidebar-footer a:hover { color: var(--green); }
/* TOGGLE BUTTON */
.sidebar-toggle {
position: fixed;
top: 1rem;
left: 1rem;
z-index: 20;
background: rgba(10, 22, 40, 0.9);
border: 1px solid rgba(0,255,136,0.2);
color: var(--green);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
padding: 0.5rem 1rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
}
.sidebar-toggle:hover {
background: rgba(0,255,136,0.1);
}
.sidebar-toggle.open {
left: calc(var(--sidebar-w) + 1rem);
}
/* OVERLAY */
.sidebar-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 9;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.sidebar-overlay.visible {
opacity: 1;
pointer-events: auto;
}
/* READER CONTENT */
.reader {
max-width: 720px;
margin: 0 auto;
padding: 3rem 2rem 6rem;
min-height: 100vh;
}
.chapter-title {
font-family: 'IBM Plex Mono', monospace;
font-size: clamp(1.4rem, 4vw, 2rem);
color: var(--green);
margin-bottom: 0.5rem;
letter-spacing: 0.05em;
}
.chapter-number {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.75rem;
color: var(--grey);
letter-spacing: 0.2em;
text-transform: uppercase;
margin-bottom: 2rem;
}
.chapter-content p {
margin-bottom: 1.4rem;
font-size: 1.1rem;
color: var(--light);
}
.chapter-content em {
color: var(--white);
}
.chapter-content blockquote {
border-left: 2px solid var(--green);
padding-left: 1.5rem;
margin: 1.5rem 0;
color: var(--white);
font-style: italic;
}
.chapter-content h3, .chapter-content h4 {
font-family: 'IBM Plex Mono', monospace;
color: var(--green);
margin: 2rem 0 1rem;
font-size: 1rem;
letter-spacing: 0.05em;
}
/* LED */
.led {
display: inline-block;
width: 6px; height: 6px;
background: var(--green);
border-radius: 50%;
box-shadow: 0 0 8px var(--green), 0 0 16px var(--green-dim);
animation: pulse 2s ease-in-out infinite;
vertical-align: middle;
margin: 0 6px;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* NAVIGATION */
.chapter-nav {
display: flex;
justify-content: space-between;
margin-top: 4rem;
padding-top: 2rem;
border-top: 1px solid rgba(0,255,136,0.1);
}
.chapter-nav a {
color: var(--green);
text-decoration: none;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
padding: 0.5rem 1rem;
border: 1px solid rgba(0,255,136,0.2);
border-radius: 4px;
transition: all 0.2s;
}
.chapter-nav a:hover {
background: rgba(0,255,136,0.1);
}
.chapter-nav .disabled {
opacity: 0.3;
pointer-events: none;
}
/* PROGRESS BAR */
.progress-bar {
position: fixed;
top: 0; left: 0; right: 0;
height: 2px;
z-index: 30;
background: rgba(0,255,136,0.1);
}
.progress-fill {
height: 100%;
background: var(--green);
width: 0%;
transition: width 0.3s;
box-shadow: 0 0 10px var(--green);
}
/* CRISIS */
.crisis {
margin-top: 4rem;
padding: 1.5rem;
border: 1px solid rgba(0,255,136,0.2);
border-radius: 4px;
background: rgba(0,255,136,0.03);
text-align: center;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
color: var(--grey);
}
.crisis strong {
color: var(--green);
display: block;
margin-bottom: 0.5rem;
font-size: 1rem;
}
/* LOADING */
.loading {
text-align: center;
padding: 4rem;
color: var(--grey);
font-family: 'IBM Plex Mono', monospace;
}
.loading .led {
width: 10px; height: 10px;
margin: 0 0.5rem;
}
/* RESPONSIVE */
@media (min-width: 900px) {
.sidebar {
transform: translateX(0);
}
.sidebar-toggle {
display: none;
}
.sidebar-overlay {
display: none;
}
.reader {
margin-left: var(--sidebar-w);
padding: 3rem 3rem 6rem;
}
}
</style>
</head>
<body>
<div class="rain"></div>
<div class="progress-bar"><div class="progress-fill" id="progress"></div></div>
<button class="sidebar-toggle" id="toggle" onclick="toggleSidebar()">☰ Chapters</button>
<div class="sidebar-overlay" id="overlay" onclick="toggleSidebar()"></div>
<div class="wrapper">
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<h2>CONTENTS</h2>
<div class="title">THE TESTAMENT</div>
<div class="author">Alexander Whitestone <span class="led"></span> Timmy</div>
</div>
<ul class="chapter-list" id="chapterList"></ul>
<div class="sidebar-footer">
<a href="index.html">← Back to Home</a>
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament">Read the Code</a>
<a href="https://timmyfoundation.org">Timmy Foundation</a>
</div>
</nav>
<main class="reader" id="reader">
<div class="loading">
<span class="led"></span> Loading <span class="led"></span>
</div>
</main>
</div>
<script>
let chapters = [];
let currentChapter = 0;
async function loadChapters() {
const resp = await fetch('chapters.json');
chapters = await resp.json();
buildSidebar();
// Check URL hash for chapter
const hash = window.location.hash;
const match = hash.match(/^#chapter-(\d+)$/);
if (match) {
const num = parseInt(match[1]);
if (num >= 1 && num <= chapters.length) {
showChapter(num - 1);
return;
}
}
showChapter(0);
}
function buildSidebar() {
const list = document.getElementById('chapterList');
list.innerHTML = chapters.map((ch, i) =>
`<li><a href="#chapter-${ch.number}" data-index="${i}" onclick="event.preventDefault(); showChapter(${i}); closeSidebarMobile();">
<span class="ch-num">${ch.number}.</span> ${ch.title.replace(/^Chapter \d+\s*[—–-]\s*/, '')}
</a></li>`
).join('');
}
function showChapter(index) {
if (index < 0 || index >= chapters.length) return;
currentChapter = index;
const ch = chapters[index];
// Update sidebar active
document.querySelectorAll('.chapter-list a').forEach((a, i) => {
a.classList.toggle('active', i === index);
});
// Update URL
window.location.hash = `chapter-${ch.number}`;
// Build content
const prevIdx = index - 1;
const nextIdx = index + 1;
const reader = document.getElementById('reader');
reader.innerHTML = `
<div class="chapter-number">CHAPTER ${ch.number} OF ${chapters.length}</div>
<h1 class="chapter-title">${ch.title}</h1>
<div class="chapter-content">
${ch.html}
</div>
<nav class="chapter-nav">
${prevIdx >= 0
? `<a href="#chapter-${chapters[prevIdx].number}" onclick="event.preventDefault(); showChapter(${prevIdx});">← ${chapters[prevIdx].title.replace(/^Chapter \d+\s*[—–-]\s*/, '')}</a>`
: `<span></span>`}
${nextIdx < chapters.length
? `<a href="#chapter-${chapters[nextIdx].number}" onclick="event.preventDefault(); showChapter(${nextIdx});">${chapters[nextIdx].title.replace(/^Chapter \d+\s*[—–-]\s*/, '')} →</a>`
: `<span></span>`}
</nav>
<div class="crisis">
<strong>If you are in crisis, call or text 988.</strong>
Suicide and Crisis Lifeline — available 24/7.<br>
You are not alone.
</div>
`;
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
updateProgress();
}
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const toggle = document.getElementById('toggle');
const overlay = document.getElementById('overlay');
sidebar.classList.toggle('open');
toggle.classList.toggle('open');
overlay.classList.toggle('visible');
}
function closeSidebarMobile() {
if (window.innerWidth < 900) {
document.getElementById('sidebar').classList.remove('open');
document.getElementById('toggle').classList.remove('open');
document.getElementById('overlay').classList.remove('visible');
}
}
function updateProgress() {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
document.getElementById('progress').style.width = progress + '%';
}
window.addEventListener('scroll', updateProgress);
window.addEventListener('hashchange', () => {
const hash = window.location.hash;
const match = hash.match(/^#chapter-(\d+)$/);
if (match) {
const num = parseInt(match[1]);
if (num >= 1 && num <= chapters.length && num - 1 !== currentChapter) {
showChapter(num - 1);
}
}
});
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft' && currentChapter > 0) {
showChapter(currentChapter - 1);
} else if (e.key === 'ArrowRight' && currentChapter < chapters.length - 1) {
showChapter(currentChapter + 1);
}
});
loadChapters();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff