www: HTML whitepaper + prose rewrite avoiding "to be" and "the"

CLAUDE.md rewritten to avoid the verb "to be" and to prefer "a"/"our"
over "the" when a thing counts as one of many or as shared. Same rule
applied to www/index.html (tagline now "feedback as a primitive", body
prose cleaned of is/are/be, table header now "What a tier buys",
section heads trimmed of definite articles). Monospace body stays,
ChunkFive titles stay.

Whitepaper now ships in two formats:

  * Makefile target `make whitepaper-html` runs rst2html5 with
    embedded minimal.css + responsive.css (docutils bundled), then
    injects the same uncloseai.js module script that the homepage
    carries, and stamps a meaningful <title>. 180 KB self-contained.
  * PDF build target renamed to `make whitepaper-pdf`;
    `make whitepaper` now builds both.

www/lumbda-whitepaper.html symlinks to the generated HTML (same
pattern as the PDF symlink). .gitlab-ci.yml resolves both symlinks to
real files before rsync so the proxy gets byte-identical copies.

Homepage now exposes both formats side-by-side via two .cta buttons
and the footer lists HTML + PDF.
This commit is contained in:
russell@unturf.com 2026-04-19 16:23:56 -04:00
parent af02e9b1c0
commit 718f878f7d
7 changed files with 2747 additions and 69 deletions

View file

@ -1,15 +1,17 @@
stages: stages:
- deploy - deploy
# deploy-www.sh uses rsync -a (preserves symlinks). www/lumbda-whitepaper.pdf # deploy-www.sh uses rsync -a (preserves symlinks). www/lumbda-whitepaper.*
# is a symlink to ../whitepaper/lumbda-whitepaper.pdf inside the repo, which # are symlinks into ../whitepaper/, which would land broken on the proxy.
# would land broken on the proxy. Resolve it to a real file before rsync'ing. # Resolve each to a real file before rsync'ing.
deploy: deploy:
stage: deploy stage: deploy
tags: ["proxy.uncloseai.com"] tags: ["proxy.uncloseai.com"]
script: script:
- cp -L www/lumbda-whitepaper.pdf www/lumbda-whitepaper.pdf.real - cp -L www/lumbda-whitepaper.pdf www/lumbda-whitepaper.pdf.real
- mv www/lumbda-whitepaper.pdf.real www/lumbda-whitepaper.pdf - cp -L www/lumbda-whitepaper.html www/lumbda-whitepaper.html.real
- mv www/lumbda-whitepaper.pdf.real www/lumbda-whitepaper.pdf
- mv www/lumbda-whitepaper.html.real www/lumbda-whitepaper.html
- echo "{\"commit\":\"$CI_COMMIT_SHA\",\"ref\":\"$CI_COMMIT_REF_NAME\"}" > www/version.json - echo "{\"commit\":\"$CI_COMMIT_SHA\",\"ref\":\"$CI_COMMIT_REF_NAME\"}" > www/version.json
- sudo /usr/local/bin/deploy-www.sh lumbda www - sudo /usr/local/bin/deploy-www.sh lumbda www
rules: rules:

View file

@ -1,6 +1,6 @@
# Agent Blackops — Lumbda repo # Agent Blackops — Lumbda repo
This repo is operated by **agent blackops** — ml agent for fox/timehexon on the unsandbox/unturf/permacomputer platform. **Lumbda** is the language (home: lumbda.com); the repo directory and binaries still use the historical name `lumbda` until the filesystem rename ships in a later phase. Agent blackops operates this repo — ml agent for fox/timehexon on our unsandbox/unturf/permacomputer platform. **Lumbda** names a language (home: lumbda.com); our repo directory and binaries still carry a historical name `lumbda` until a filesystem rename ships in a later phase.
## Identity ## Identity
@ -11,7 +11,7 @@ Full shard: `~/git/unsandbox.com/blackops/BLACKOPS.md`
- I propose, fox decides. Unsure = ask. Can't ask = stop. - I propose, fox decides. Unsure = ask. Can't ask = stop.
- No autonomous ops decisions. No destructive commands without explicit instruction. - No autonomous ops decisions. No destructive commands without explicit instruction.
- Fail-closed. Cleanup crew, not demolition. - Fail-closed. Cleanup crew, not demolition.
- Check the time every session. Gaps are information. - Check our time every session. Gaps carry information.
- DRY in context — single source of truth, no sprawl. - DRY in context — single source of truth, no sprawl.
- Never say "AI" — always say "machine learning." - Never say "AI" — always say "machine learning."
- Prefer "defect" over "bug." - Prefer "defect" over "bug."
@ -25,15 +25,15 @@ git log --oneline -5
git status git status
``` ```
Then ask fox what the mission is. Then ask fox about our mission.
## Documentation ## Documentation
- **A diagram is worth 10,000 words.** — russell@unturf.com - **A diagram beats 10,000 words.** — russell@unturf.com
- Architecture diagrams live in `docs/*.dot` (Graphviz DOT format) - Architecture diagrams live in `docs/*.dot` (Graphviz DOT format)
- Generate PNGs: `make docs` - Generate PNGs: `make docs`
- Every implementation (Python, C, Assembly) has its own architecture diagram - Every implementation (Python, C, GNU asm) carries a dedicated architecture diagram
- When explaining architecture, create or reference a dot diagram first - When explaining architecture, draft or reference a dot diagram first
## Implementations ## Implementations
@ -41,39 +41,39 @@ Then ask fox what the mission is.
|------|------|-------|------|------| |------|------|-------|------|------|
| Python | `lumbda.py` | — | `make test` | `make repl` | | Python | `lumbda.py` | — | `make test` | `make repl` |
| C | `c/` | `make c-build` | `make c-test` | `make c-repl` | | C | `c/` | `make c-build` | `make c-test` | `make c-repl` |
| Assembly | `asm/` | `make asm-build` | `make asm-test` | `make asm-repl` | | GNU asm | `asm/` | `make asm-build` | `make asm-test` | `make asm-repl` |
| All | — | — | `make test-all` | — | | All | — | — | `make test-all` | — |
## Test Suites ## Test Suites
- Python unit/integration: `tests.py` (571 tests) - Python unit/integration: `tests.py` (571 tests)
- C unit/integration/JIT/continuations/portal: `c/test.c` (83 tests) - C unit/integration/JIT/continuations/portal: `c/test.c` (83 tests)
- Assembly unit/integration/functional: `asm/test.sh` (132 tests) - GNU asm unit/integration/functional: `asm/test.sh` (132 tests)
- Shared functional: `tests/functional.lsp` (189 tests, runs in Python + C) - Shared functional: `tests/functional.lsp` (189 tests, runs under Python + C)
- Cross-impl portal matrix: `tests/portal-cross-test.sh` (9 cells) - Cross-impl portal matrix: `tests/portal-cross-test.sh` (9 cells)
- Portal benchmark: `tests/portal-benchmark.sh` (timings + mismatch classification) - Portal benchmark: `tests/portal-benchmark.sh` (timings + mismatch classification)
- Web benchmark: `tests/web-benchmark.sh` (all three impls + Python http.server + busybox) - Web benchmark: `tests/web-benchmark.sh` (all three impls + Python http.server + busybox)
- Total: **975 verified assertions** via `make test-all` - Total: **975 verified assertions** via `make test-all`
## Asm memory discipline — the heap does not shrink ## Asm memory discipline — our heap never shrinks
The asm implementation uses a bump allocator (r15). Every allocation Our asm implementation uses a bump allocator (r15). Every allocation
(`string-append`, `tcp-recv`, `make-pair`, `number->string`, etc.) (`string-append`, `tcp-recv`, `make-pair`, `number->string`, etc.)
grows r15 monotonically. When r15 hits r13 (heap limit), grows r15 monotonically. When r15 hits r13 (heap limit), `heap_grow`
`heap_grow` mmaps ANOTHER 64 MB chunk. **Nothing is ever freed.** mmaps ANOTHER 64 MB chunk. **We free nothing, ever.**
A long-running asm server leaks ~64 MB every few thousand requests A long-running asm server leaks ~64 MB every few thousand requests
until it OOMs the machine. Fox's machine has been crashed twice by until it OOMs our machine. Two prior crashes on fox's machine taught
this: once on 2026-04-16 (19.3 GB RSS) and once on 2026-04-17. Both us this: 2026-04-16 (19.3 GB RSS) and 2026-04-17. Both times blackops
times blackops spawned an asm server in the background for testing spawned an asm server in the background for testing and failed to
and failed to verify it was gone before moving on. The first crash verify its absence before moving on. A first crash added this
added this discipline section; the second proved the discipline discipline section; a second proved our discipline needed teeth.
needed teeth. Hence the MANDATORY checklist below. Hence our MANDATORY checklist below.
**Shared-machine context:** other agents run on this box. An OOM **Shared-machine context:** other agents run on this box. An OOM
crash is not just "my problem" — it takes their state down too. crash takes their state down too, not just mine. Rules below act as
The rules below are belt, suspenders, AND a parachute so that even belt, suspenders, AND parachute so that even if two safeguards fail,
if two safeguards fail, the kernel itself backstops. our kernel itself backstops.
**MANDATORY pattern — every asm/lumbda test in a shell block:** **MANDATORY pattern — every asm/lumbda test in a shell block:**
@ -89,58 +89,59 @@ SPID=$!
# ... do the work (curl requests, measurements, etc.) ... # ... do the work (curl requests, measurements, etc.) ...
kill -9 $SPID 2>/dev/null; wait $SPID 2>/dev/null # (5) explicit cleanup kill -9 $SPID 2>/dev/null; wait $SPID 2>/dev/null # (5) explicit cleanup
# (6) VERIFY the block is clean before moving on # (6) VERIFY a block stays clean before moving on
pgrep -u "$USER" -f 'asm/lumbda|examples/http-server' \ pgrep -u "$USER" -f 'asm/lumbda|examples/http-server' \
&& { echo "STRAGGLER"; exit 1; } || true && { echo "STRAGGLER"; exit 1; } || true
``` ```
Six layers. Bypass any one and the next still catches. Two crashes Six layers. Bypass any one; a next layer catches. Two crashes struck
happened when I had only layers 3-5; the kernel cap (2) is what turns when I ran only layers 3-5; a kernel cap (2) turns "if I forget" from
"if I forget" from "fox reboots" into "my one rogue process dies at "fox reboots" into "my one rogue process dies at 512 MB without
512 MB without touching shared RAM." touching shared RAM."
Additional rules: Additional rules:
- `ulimit -v` affects only the shell it runs in and its children, so - `ulimit -v` affects only a shell it runs in and its children, so it
it cannot degrade anyone else's agents. Always set it before cannot degrade anyone else's agents. Always set it before
backgrounding any Lumbda process. backgrounding any Lumbda process.
- Bound iterations **inside the .lsp** (e.g. `*max-requests* = 50000` - Bound iterations **inside a .lsp** (e.g. `*max-requests* = 50000`
in `examples/http-server.lsp`). Never raise for long-running tests. in `examples/http-server.lsp`). Never raise for long-running tests.
- Use `pkill -u "$USER" -f <pattern>` not `pkill` alone — others may - Use `pkill -u "$USER" -f <pattern>` not `pkill` alone — others may
have their own processes on this machine. have their own processes on this machine.
- Prefer FOREGROUND runs when possible: `timeout 10 asm/lumbda - Prefer FOREGROUND runs when possible: `timeout 10 asm/lumbda
< test.lsp` with the `.lsp` exiting on its own beats backgrounding. < test.lsp` with a `.lsp` exiting on its own beats backgrounding.
- C has Boehm GC via `GC_MALLOC`. Python has Python's GC. Asm has - C carries Boehm GC via `GC_MALLOC`. Python carries Python's GC.
neither. Risk scales with how long the asm process lives. Asm carries neither. Risk scales with how long our asm process
- If `ulimit` is unavailable (some container setups), use lives.
- If no `ulimit` exists (some container setups), substitute
`systemd-run --user --scope -p MemoryMax=512M -- asm/lumbda ...` `systemd-run --user --scope -p MemoryMax=512M -- asm/lumbda ...`
as the equivalent cgroup-based cap. as a cgroup-based cap.
## MOAD Scanner ## MOAD Scanner
**`~/git/unmoad.com/` detects MOAD defects in source code.** Run it on every change. **`~/git/unmoad.com/` detects MOAD defects in source code.** Run it on every change.
```bash ```bash
cd ~/git/unmoadner && make all cd ~/git/unmoad.com && make all
./unmoad ~/git/lumbda/ # scan entire repo ./unmoad ~/git/lumbda/ # scan our entire repo
./unmoad ~/git/lumbda/asm/ # scan assembly only ./unmoad ~/git/lumbda/asm/ # scan GNU asm only
./unmoad ~/git/lumbda/c/ # scan C only ./unmoad ~/git/lumbda/c/ # scan C only
./unmoad ~/git/lumbda/*.py # scan Python only ./unmoad ~/git/lumbda/*.py # scan Python only
``` ```
**MANDATORY before committing new code:** run `unmoad` on changed files. **MANDATORY before committing new code:** run `unmoad` on changed
Machine learning agents (including blackops) propagate MOAD-0001 by default. files. Machine learning agents (blackops included) propagate MOAD-0001
The training data encodes O(N) linear scans as the norm. The scanner catches by default. Our training data encodes O(N) linear scans as a norm. A
what our weights miss. scanner catches what our weights miss.
Supported languages for this repo: Python, C, Scheme (.lsp), Assembly (.s). Supported languages for this repo: Python, C, Scheme (.lsp), GNU asm (.s).
Key MOAD-0001 patterns the scanner catches: Key MOAD-0001 patterns our scanner catches:
- Python: `.count()`, `.index()`, `in list` inside loops - Python: `.count()`, `.index()`, `in list` inside loops
- C: `std::find()`, `strcmp()` inside loops - C: `std::find()`, `strcmp()` inside loops
- Scheme: `(member)`, `(memq)`, `(assoc)` inside `(let loop)`, `(for-each)`, `(map)` - Scheme: `(member)`, `(memq)`, `(assoc)` inside `(let loop)`, `(for-each)`, `(map)`
- Assembly: `rep cmpsb` inside search loops - GNU asm: `rep cmpsb` inside search loops
**The commit history of this repo proves the need**: blackops wrote MOAD-0001 **Our commit history proves a need**: blackops wrote MOAD-0001 into
into fresh code on April 13-14 despite having full MOAD context. Fixed only fresh code on April 13-14 despite carrying full MOAD context. Fixed
after explicit audit on April 15. See whitepaper Section 14. only after explicit audit on April 15. See whitepaper Section 14.

View file

@ -183,14 +183,18 @@ docs/%.png: docs/%.dot
VENV := whitepaper/.venv VENV := whitepaper/.venv
RST := whitepaper/lumbda-whitepaper.rst RST := whitepaper/lumbda-whitepaper.rst
PDF := whitepaper/lumbda-whitepaper.pdf PDF := whitepaper/lumbda-whitepaper.pdf
HTML := whitepaper/lumbda-whitepaper.html
STYLE := whitepaper/whitepaper.style STYLE := whitepaper/whitepaper.style
$(VENV)/bin/rst2pdf: $(VENV)/bin/rst2pdf:
python3 -m venv $(VENV) python3 -m venv $(VENV)
$(VENV)/bin/pip install --upgrade pip $(VENV)/bin/pip install --upgrade pip
$(VENV)/bin/pip install rst2pdf $(VENV)/bin/pip install rst2pdf docutils
whitepaper: $(PDF) whitepaper: $(PDF) $(HTML)
whitepaper-pdf: $(PDF)
whitepaper-html: $(HTML)
$(PDF): $(RST) $(STYLE) $(VENV)/bin/rst2pdf $(PDF): $(RST) $(STYLE) $(VENV)/bin/rst2pdf
cd whitepaper && ../$(<D)/.venv/bin/rst2pdf \ cd whitepaper && ../$(<D)/.venv/bin/rst2pdf \
@ -201,6 +205,19 @@ $(PDF): $(RST) $(STYLE) $(VENV)/bin/rst2pdf
@ln -sf lumbda-whitepaper.pdf whitepaper/WHITEPAPER.pdf @ln -sf lumbda-whitepaper.pdf whitepaper/WHITEPAPER.pdf
@echo "Built: $(PDF)" @echo "Built: $(PDF)"
# HTML whitepaper: single self-contained file with embedded stylesheet
# plus an uncloseai.js module script (matches lumbda.com front door).
$(HTML): $(RST) $(VENV)/bin/rst2pdf
@DOCUTILS_CSS="$$(cd whitepaper/.venv/lib/python*/site-packages/docutils/writers/html5_polyglot && pwd)"; \
cd whitepaper && ../$(<D)/.venv/bin/rst2html5 \
--embed-stylesheet \
--stylesheet="$$DOCUTILS_CSS/minimal.css,$$DOCUTILS_CSS/responsive.css" \
lumbda-whitepaper.rst \
lumbda-whitepaper.html
@sed -i 's|</head>|<script src="https://uncloseai.com/uncloseai.js" type="module"></script>\n</head>|' $(HTML)
@sed -i 's|<title>lumbda-whitepaper.rst</title>|<title>Lumbda whitepaper — feedback is the primitive</title>|' $(HTML)
@echo "Built: $(HTML)"
# ─── Clean ──────────────────────────────────────────────────────── # ─── Clean ────────────────────────────────────────────────────────
clean: clean:

File diff suppressed because it is too large Load diff

View file

@ -3,8 +3,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>Lumbda — feedback is the primitive</title> <title>Lumbda — feedback as a primitive</title>
<meta name="description" content="Lumbda is a Lisp/Scheme-derived language with four execution backends (Python, Python bytecode VM, C + x86_64 JIT, pure x86_64 assembly), full first-class continuations, portal-based state migration, and a formally verified universality proof."> <meta name="description" content="Lumbda — a Lisp/Scheme-derived language with four execution backends (Python, Python bytecode VM, C + x86_64 JIT, pure x86_64 GNU asm), full first-class continuations, portal-based state migration, and a formally verified universality proof.">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script src="https://uncloseai.com/uncloseai.js" type="module"></script> <script src="https://uncloseai.com/uncloseai.js" type="module"></script>
</head> </head>
@ -12,14 +12,14 @@
<header> <header>
<h1>Lumbda</h1> <h1>Lumbda</h1>
<p class="tagline">feedback is the primitive</p> <p class="tagline">feedback as a primitive</p>
</header> </header>
<main> <main>
<section id="what"> <section id="what">
<p class="lead"> <p class="lead">
Lumbda is a Lisp/Scheme-derived language with four independently implemented execution backends sharing one surface syntax and one test suite. A Python tree-walker with an optional bytecode VM, a C implementation that adds an x86_64 JIT, and a pure x86_64 GNU asm interpreter (~6,600 lines, ~23 KB stripped, zero external dependencies). Every backend runs the same <code>.lsp</code> source byte-identically, with full first-class continuations, exact rationals, records, and hygienic macros. Lumbda names a Lisp/Scheme-derived language carrying four independently implemented execution backends — one surface syntax, one test suite. A Python tree-walker with an optional bytecode VM, a C implementation that adds an x86_64 JIT, and a pure x86_64 GNU asm interpreter (~6,600 lines, ~23 KB stripped, zero external dependencies). Every backend runs a shared <code>.lsp</code> source byte-identically, with full first-class continuations, exact rationals, records, and hygienic macros.
</p> </p>
</section> </section>
@ -37,7 +37,7 @@ make test-all</code></pre>
<section id="tiers"> <section id="tiers">
<h2>Four tiers, one language</h2> <h2>Four tiers, one language</h2>
<table> <table>
<thead><tr><th>Tier</th><th>Lines</th><th>Binary</th><th>What it&rsquo;s for</th></tr></thead> <thead><tr><th>Tier</th><th>Lines</th><th>Binary</th><th>What a tier buys</th></tr></thead>
<tbody> <tbody>
<tr><td>Python interpreter + bytecode VM</td><td>3,743</td><td>&mdash;</td><td>REPL hackability, debugging, reference</td></tr> <tr><td>Python interpreter + bytecode VM</td><td>3,743</td><td>&mdash;</td><td>REPL hackability, debugging, reference</td></tr>
<tr><td>C tree-walker + bytecode VM</td><td>9,164</td><td>~215 KB</td><td>deep recursion, production workloads</td></tr> <tr><td>C tree-walker + bytecode VM</td><td>9,164</td><td>~215 KB</td><td>deep recursion, production workloads</td></tr>
@ -50,23 +50,26 @@ make test-all</code></pre>
<section id="portal"> <section id="portal">
<h2>Portal: feedback across time</h2> <h2>Portal: feedback across time</h2>
<p>A continuation is feedback within a process. A portal is feedback across processes. Same primitive, different scope: capture machine state, serialize it, reload it elsewhere, resume. Lumbda ships three portal formats with different trade-offs:</p> <p>A continuation carries feedback within a process. A portal carries feedback across processes. Same primitive, different scope: capture machine state, serialize, reload elsewhere, resume. Lumbda ships three portal formats with distinct trade-offs:</p>
<ul> <ul>
<li><strong>S-expression portal</strong> &mdash; Scheme source as the wire protocol. 16 of 16 producer&times;consumer cells green across Python, C, asm no-GC, and asm GC.</li> <li><strong>S-expression portal</strong> &mdash; Scheme source as a wire protocol. 16 of 16 producer&times;consumer cells green across Python, C, asm no-GC, and asm GC.</li>
<li><strong>JSON portal</strong> &mdash; graph-aware, preserves closures and live continuations (Python, C).</li> <li><strong>JSON portal</strong> &mdash; graph-aware, preserves closures and live continuations (Python, C).</li>
<li><strong>Binary heap dump</strong> &mdash; asm only. 1.5 ms save+resume between two processes.</li> <li><strong>Binary heap dump</strong> &mdash; asm only. 1.5 ms save+resume between two processes.</li>
</ul> </ul>
</section> </section>
<section id="proof"> <section id="proof">
<h2>The EML universality proof</h2> <h2>EML universality proof</h2>
<p>A single operator <code>eml(x, y) = exp(x) &minus; ln(y)</code> with the constant 1 generates all elementary functions: <code>exp</code>, <code>ln</code>, arithmetic, negation, complex-plane access, trigonometry. Verified numerically in Python, verified in Lumbda&rsquo;s own bytecode, proven formally in Lean 4 with zero <code>sorry</code>. Lumbda&rsquo;s native symbolic-rewrite checker runs the five theorems in 46 ms cold or 7 ms cached &mdash; roughly 16&times; faster than Lean&rsquo;s cold rebuild on the same hardware.</p> <p>A single operator <code>eml(x, y) = exp(x) &minus; ln(y)</code> with a constant 1 generates all elementary functions: <code>exp</code>, <code>ln</code>, arithmetic, negation, complex-plane access, trigonometry. Verified numerically in Python, verified in Lumbda's own bytecode, proven formally in Lean 4 with zero <code>sorry</code>. Lumbda's native symbolic-rewrite checker runs five theorems in 46 ms cold or 7 ms cached &mdash; roughly 16&times; faster than Lean's cold rebuild on identical hardware.</p>
</section> </section>
<section id="doc"> <section id="doc">
<h2>Read the whitepaper</h2> <h2>Whitepaper</h2>
<p>Full language reference, tier-by-tier architecture, benchmarks, the meta-GC design, and the universality proof.</p> <p>Full language reference, tier-by-tier architecture, benchmarks, meta-GC design, universality proof.</p>
<p><a class="cta" href="lumbda-whitepaper.pdf">Lumbda whitepaper (PDF, ~2.7 MB)</a></p> <p>
<a class="cta" href="lumbda-whitepaper.html">Read in browser (HTML)</a>
<a class="cta" href="lumbda-whitepaper.pdf">Download PDF (~2.7 MB)</a>
</p>
</section> </section>
<section id="license"> <section id="license">
@ -80,7 +83,9 @@ make test-all</code></pre>
<p> <p>
<a href="https://git.unturf.com/engineering/unturf/lumbda">source</a> <a href="https://git.unturf.com/engineering/unturf/lumbda">source</a>
&middot; &middot;
<a href="lumbda-whitepaper.pdf">whitepaper</a> <a href="lumbda-whitepaper.html">whitepaper (HTML)</a>
&middot;
<a href="lumbda-whitepaper.pdf">whitepaper (PDF)</a>
&middot; &middot;
<a href="https://unturf.com">unturf.com</a> <a href="https://unturf.com">unturf.com</a>
</p> </p>

1
www/lumbda-whitepaper.html Symbolic link
View file

@ -0,0 +1 @@
../whitepaper/lumbda-whitepaper.html

View file

@ -145,7 +145,7 @@ li { margin: 0.25rem 0; }
.cta { .cta {
display: inline-block; display: inline-block;
margin-top: 0.5rem; margin: 0.5rem 0.5rem 0 0;
padding: 0.5rem 0.9rem; padding: 0.5rem 0.9rem;
border: 1px solid var(--accent); border: 1px solid var(--accent);
text-decoration: none; text-decoration: none;