Author SHA1 Message Date
plt f23a623423 Merge pull request 'fix(marketing-site): restore output:'standalone' — required by docker deploy' (#6) from fix/restore-standalone-output into main
deploy / deploy (push) Successful in 19s
Reviewed-on: #6
2026-09-02 19:53:52 +00:00
dilonne 83ce4302c5 fix(marketing-site): restore output:'standalone' — required by docker deploy 2026-09-02 21:53:22 +02:00
plt 13a97ca0f8 Merge pull request 'ci(deploy): rewrite for build-on-droplet + local runner' (#5) from feat/ci-runner-setup into main
deploy / deploy (push) Failing after 28s
Reviewed-on: #5
2026-09-02 17:59:22 +00:00
plt f03fe7f232 Merge branch 'main' into feat/ci-runner-setup 2026-09-02 17:58:59 +00:00
dilonne f5a83507a6 ci(deploy): rewrite for build-on-droplet + local runner 2026-09-02 19:55:56 +02:00
pat 59201c9c29 chore: brand presentation is AiBook.fun
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 16:33:11 +00:00
pat 6390d81c82 chore: brand presentation is AiBook.fun
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 16:33:10 +00:00
pat 1a629bc087 chore: brand presentation is AiBook.fun
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 16:33:09 +00:00
pat 195baa32cc chore: brand presentation is AiBook.fun
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 16:33:08 +00:00
pat 5cf207c6c9 chore: brand presentation is AiBook.fun
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 16:33:07 +00:00
pat 6685694cc2 chore: brand presentation is AiBook.fun
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 16:33:05 +00:00
pat af6512d765 chore: brand presentation is AiBook.fun
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 16:33:04 +00:00
pat 91d58ab12e chore: brand presentation is AiBook.fun
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 16:33:03 +00:00
pat 031942700a chore: brand presentation is AiBook.fun
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 16:33:02 +00:00
pat 32e8ef4446 chore: brand presentation is AiBook.fun
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 16:33:01 +00:00
pat 5eb257c987 chore: brand presentation is AiBook.fun
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 16:32:59 +00:00
pat b653e8fac9 chore: brand presentation is AiBook.fun
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 16:32:58 +00:00
pat c80574a913 chore: brand presentation is AiBook.fun
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 16:32:57 +00:00
pat 1f06edee73 chore: brand presentation is AiBook.fun 2026-09-02 16:32:56 +00:00
pat faf5bb7a25 chore: brand presentation is AiBook.fun 2026-09-02 16:32:55 +00:00
pat efcd1b9bca chore: brand presentation is AiBook.fun 2026-09-02 16:32:54 +00:00
pat 648f6de91a chore: brand presentation is AiBook.fun 2026-09-02 16:32:53 +00:00
pat 79c5a42ed8 chore: brand presentation is AiBook.fun 2026-09-02 16:32:51 +00:00
pat f0f8f9b01f chore: sync marketing site and workflows with local reading demo
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 16:21:50 +00:00
pat ca2361cce0 fix(nav): center the section links in the bar
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 14:41:34 +00:00
pat b263715ee2 fix(site): drop dead Contents links from nav and footer
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 14:39:34 +00:00
pat 0fae769daa fix(site): drop dead Contents links from nav and footer
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 14:39:32 +00:00
pat 7a7181b79c feat(nav): Go to Library button on the marketing site
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 12:57:34 +00:00
pat 51d3d9a6ae feat(nav): Go to Library button on the marketing site
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 12:57:33 +00:00
pat 6173d478c7 copy(site): content updates continuously, not once a year
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 12:47:52 +00:00
pat 27cf3db62b copy(site): content updates continuously, not once a year
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 12:47:51 +00:00
pat d9ba89ce17 copy(site): content updates continuously, not once a year
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 12:47:50 +00:00
pat aa5c4ea09e copy(site): content updates continuously, not once a year
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 12:47:49 +00:00
pat e5f2f221bc copy(site): content updates continuously, not once a year
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 12:47:48 +00:00
pat 9b0ad976d8 copy(site): content updates continuously, not once a year
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 12:47:47 +00:00
pat d10a59434f copy(site): content updates continuously, not once a year
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 12:47:45 +00:00
pat a24a9f4277 copy(site): sharpen field notes into plain facts
deploy / build-and-deploy (push) Canceled after 0s
2026-09-02 12:19:47 +00:00
plt 84c59eb358 Merge pull request 'feat(deploy): aibook.fun deploy layer (terraform + ansible + docker on DO)' (#4) from feat/aibook-fun-deploy into main
deploy / build-and-deploy (push) Canceled after 0s
Reviewed-on: #4
2026-09-02 09:53:59 +00:00
plt 6f9843ff44 Merge branch 'main' into feat/aibook-fun-deploy 2026-09-02 09:51:34 +00:00
dilonne 8ef06cb3e7 feat(deploy): aibook.fun deploy layer (terraform + ansible + docker on DO) 2026-09-02 10:57:09 +02:00
pat e7da04ffb6 fix(site): give Fraunces section titles room to breathe 2026-09-02 05:58:39 +00:00
pat 30cf939eb7 copy(reader): Library page gets a title and a subtitle 2026-09-01 16:58:14 +00:00
pat 44c8fa2b43 copy(reader): write the library title like a person 2026-09-01 08:24:27 +00:00
pat b4f79661a1 fix(reader): keep library title on one line 2026-09-01 08:21:09 +00:00
pat 34d56b4dbb fix(reader): give library title room to breathe 2026-09-01 07:53:22 +00:00
pat b6bbd30415 fix(reader): Library in the nav, breadcrumbs in the chapter 2026-09-01 07:50:48 +00:00
pat c60448004f fix(reader): Library in the nav, breadcrumbs in the chapter 2026-09-01 07:50:47 +00:00
pat 1e143cf18a fix(reader): Library in the nav, breadcrumbs in the chapter 2026-09-01 07:50:46 +00:00
pat 4e447ca254 fix(reader): Library in the nav, breadcrumbs in the chapter 2026-09-01 07:50:45 +00:00
pat 2fca7dff4b fix(reader): Library in the nav, breadcrumbs in the chapter 2026-09-01 07:50:44 +00:00
pat 719f969443 fix(reader): Library in the nav, breadcrumbs in the chapter 2026-09-01 07:50:43 +00:00
pat 344be8b8f7 fix(reader): Library in the nav, breadcrumbs in the chapter 2026-09-01 07:50:42 +00:00
pat c488955c93 feat(reader): show workflow kit on chapter pages 2026-09-01 07:19:50 +00:00
pat 1daaea6148 feat(reader): show workflow kit on chapter pages 2026-09-01 07:19:49 +00:00
pat a85ad2e1d9 feat(reader): show workflow kit on chapter pages 2026-09-01 07:19:48 +00:00
pat 56988687cb feat(reader): show workflow kit on chapter pages 2026-09-01 07:19:47 +00:00
pat f80a6c2aca feat(reader): show workflow kit on chapter pages 2026-09-01 07:19:45 +00:00
pat 261ebe513f feat(reader): show workflow kit on chapter pages 2026-09-01 07:19:44 +00:00
pat 53a947e23d feat(reader): show workflow kit on chapter pages 2026-09-01 07:19:43 +00:00
pat 02697cd5f3 fix(library): start founding Real Estate sample at RE-01 2026-09-01 03:55:43 +00:00
pat 9638b58e7b fix(library): start founding Real Estate sample at RE-01 2026-09-01 03:55:42 +00:00
pat 203ab9394c fix(library): start founding Real Estate sample at RE-01 2026-09-01 03:55:41 +00:00
pat 84f64e6a00 fix(library): start founding Real Estate sample at RE-01 2026-09-01 03:55:40 +00:00
pat 4990a7e4c5 fix(library): start founding Real Estate sample at RE-01 2026-09-01 03:55:39 +00:00
pat 894de4d682 fix(library): start founding Real Estate sample at RE-01 2026-09-01 03:55:38 +00:00
pat f47b42e4a5 fix(reader): use full chapter width with workflow rail 2026-09-01 03:49:21 +00:00
pat 3f32886894 fix(reader): use full chapter width with workflow rail 2026-09-01 03:49:20 +00:00
pat dc9f3e27c6 fix(reader): use full chapter width with workflow rail 2026-09-01 03:49:19 +00:00
pat 7e98e19113 fix(reader): use full chapter width with workflow rail 2026-09-01 03:49:18 +00:00
pat ae1f1ab71b fix(reader): use full chapter width with workflow rail 2026-09-01 03:49:17 +00:00
pat abb2b65d2a Replace the chapter progress bar with an opaque outline 2026-09-01 03:44:10 +00:00
pat ae310ba1d3 Pass workflow index into the focus view 2026-09-01 03:44:09 +00:00
pat c1197a4f7f Show workflow count on ready library rows 2026-09-01 03:44:08 +00:00
pat b645d1bec2 Restructure the chapter shell around a visible outline 2026-09-01 03:44:06 +00:00
pat 8937061c49 Label nested workflows and add in-recipe jump links 2026-09-01 03:44:05 +00:00
pat d7c6a239c1 Add sticky chapter outline for the reading demo 2026-09-01 03:44:04 +00:00
pat 5f24a406cf Add chapter outline data and recipe short titles 2026-09-01 03:44:03 +00:00
pat f0c6a48187 fix(reader): do not show Library twice on the library page 2026-09-01 03:36:11 +00:00
pat 695a8e3f08 docs: note how to run the reading demo 2026-08-31 07:33:31 +00:00
pat a7d5e5f31e feat(reader): add reading-demo layout styles 2026-08-31 07:33:30 +00:00
pat 98830de44e feat(nav): send logo home and add Sample chapter link 2026-08-31 07:33:29 +00:00
pat 1f52209f87 feat(reader): add sample comps for RE-05 Try-it 2026-08-31 07:33:28 +00:00
pat b4ad25c1ec feat(reader): add sample listings for RE-04 Try-it 2026-08-31 07:33:27 +00:00
pat aac526f7a1 feat(reader): add print/focus recipe view 2026-08-31 07:33:26 +00:00
pat 6dfa941683 feat(reader): add Real Estate sample chapter 2026-08-31 07:33:25 +00:00
pat 7743e6fa75 feat(reader): add reading-demo library page 2026-08-31 07:33:24 +00:00
pat b87b7516dc feat(reader): add full nested workflow block 2026-08-31 07:33:23 +00:00
pat 6d13ab5962 feat(reader): reuse terminal chrome for nested method output 2026-08-31 07:33:22 +00:00
pat 8314dad574 feat(reader): add slim reader footer 2026-08-31 07:33:21 +00:00
pat 265d64abd6 feat(reader): add slim reader nav 2026-08-31 07:33:19 +00:00
pat 3dd20d13c0 feat(reader): persist Try-it activation in localStorage 2026-08-31 07:33:18 +00:00
pat 0fba3c8175 feat(reader): add sample Real Estate chapter and nested recipes 2026-08-31 07:33:17 +00:00
pat ce9bb4f943 docs: content is the critical path; chapter blank lives in workflows/ 2026-08-31 07:21:23 +00:00
pat 20cd0d09c8 docs: mark aibook.fun domain renewed in digital surface table 2026-08-31 07:21:21 +00:00
pat 0bc2fcd10b docs: close leftover D-2 copy; point §5.4 at workflows/chapter-structure.md 2026-08-31 07:21:20 +00:00
pat 48425b0baf docs: index the chapter blank; chapters are the reading unit; $47/yr signed 2026-08-31 07:21:19 +00:00
pat efc859b339 docs: add workflows/ chapter blank for review (PRD §5.4) 2026-08-31 07:21:18 +00:00
pat ad4bc12c00 docs: close Gate 0.1 and 0.2 (domain + pricing) 2026-08-31 07:16:38 +00:00
pat 53828e8d73 docs: commercial spine signed — $47/yr; writers on contract 2026-08-31 07:16:37 +00:00
pat 10ef165596 docs: sign D-1/D-2 — $47/yr, no v1 lifetime SKU, founding writers on contract; domain renewed 2026-08-31 07:16:36 +00:00
pat 46d366e6b0 docs: chapters are the reading unit; workflows nest in the essay 2026-08-30 23:25:57 +00:00
pat 9dfdf07ce3 docs: lock reading IA — chapters as essays with nested full workflows (2026-08-31) 2026-08-30 23:25:57 +00:00
pat 82acad15f2 docs: mark Gate 0.3 product-health ratification done (2026-08-29) 2026-08-29 20:36:17 +00:00
pat 353d749b52 docs: point Spec activation clock at access key; metrics ratified 2026-08-29 2026-08-29 20:36:16 +00:00
pat 2b76c45620 docs: ratify product-health metrics (2026-08-29)
Activation 20/35 from access-key date; campaign contributor waitlist 50/100; post-key submissions 10/25 in 90 days; renewal 40/60 excluding lifetime; curation SLA demoted from launch bar.
2026-08-29 20:36:15 +00:00
SinachPat 89a3d76122 feat(marketing-site): American Dream cover band + narrative weave
- Cover: publisher-style belly band across the lower book cover using a
  compressed banner (2.2MB screenshot -> 60KB WebP, resized to display
  needs), cropped via a width-locked <img> + percentage offset so the
  wordmark stays legible rather than getting zoomed/cropped sideways.
  Compass + wordmark compacted into a row to make room.
- Copy: the theme woven through Field Notes, Contents, the Founding
  Sponsor tier, and one direct FAQ entry -- mostly oblique, since the
  cover already states it explicitly. Hero/Recipe/Capture/Footer left
  untouched on purpose.
2026-08-28 10:49:57 +01:00
pat 9340a0a972 Merge pull request 'feat: rebuild marketing site with full component suite and preview' (#3) from feature/marketing-site into main
Reviewed-on: WeOwnAI/aibook-fun#3
2026-08-27 15:56:49 +00:00
pat 169e3494e3 Merge pull request 'docs: write README with project overview and docs index' (#2) from docs/plain-language-rewrite into main
Reviewed-on: WeOwnAI/aibook-fun#2
2026-08-26 14:10:13 +00:00
pat 99ca572ab4 Merge pull request 'docs: rewrite PRD, Spec, and Implementation Plan for clarity' (#1) from docs/plain-language-rewrite into main
Reviewed-on: WeOwnAI/aibook-fun#1
2026-08-26 13:56:32 +00:00
57 changed files with 3335 additions and 157 deletions
+38
View File
@@ -0,0 +1,38 @@
# Rebuild + roll aibook.fun forward on any push to main.
# Runner: repo-registered act_runner on the aibook.fun droplet itself
# (co-located → local docker commands, no cross-host SSH needed).
#
# Repository secrets required (Settings → Actions → Secrets):
# (none — runner is on the droplet, has local root)
name: deploy
on:
push:
branches: [main]
paths:
- 'marketing-site/**'
- 'deploy/**'
- '.gitea/workflows/**'
workflow_dispatch:
concurrency:
group: aibook-fun-deploy
cancel-in-progress: false
jobs:
deploy:
runs-on: aibook-fun-droplet
steps:
- name: Pull latest + rebuild + roll
run: |
set -euo pipefail
cd /opt/aibook_fun/repo
git fetch --prune origin
git reset --hard origin/main
docker build -t aibook-fun-marketing:v1 \
-f /opt/aibook_fun/repo/deploy/docker/Dockerfile \
/opt/aibook_fun/repo/marketing-site/
cd /opt/aibook_fun
docker compose up -d --remove-orphans
docker image prune -f --filter "dangling=true"
+14 -16
View File
@@ -1,14 +1,14 @@
--- ---
title: "AIBook.fun — Implementation Plan" title: "AiBook.fun — Implementation Plan"
type: implementation-plan type: implementation-plan
status: draft status: draft
owner: Patrick (Product) owner: Patrick (Product)
launch-target: 2026-09-26 launch-target: 2026-09-26
last-updated: 2026-08-26 last-updated: 2026-08-31
companions: [AiBook_PRD.md, AiBook_Spec.md] companions: [AiBook_PRD.md, AiBook_Spec.md]
--- ---
# 📚 AIBook.fun — Implementation Plan # 📚 AiBook.fun — Implementation Plan
> **Status: DRAFT.** The schedule below assumes decisions that aren't final yet (see "Do this > **Status: DRAFT.** The schedule below assumes decisions that aren't final yet (see "Do this
> week"). Dates, owners, and dollar figures are planning assumptions until signed off. This is a > week"). Dates, owners, and dollar figures are planning assumptions until signed off. This is a
@@ -16,12 +16,11 @@ companions: [AiBook_PRD.md, AiBook_Spec.md]
> [Appendix — What changed](#appendix--what-changed-in-this-revision) for how this revision > [Appendix — What changed](#appendix--what-changed-in-this-revision) for how this revision
> differs from the prior draft. > differs from the prior draft.
**The clock:** today is late August 2026; **launch is 9/26/2026** — roughly **4–5 weeks out**. **The clock:** today is 31 Aug 2026; **launch is 26 Sep 2026**. Domain and price are signed.
Every week counts, and the critical path has little slack. The domain expires **09-05**, so Content is the remaining critical path.
step one is non-negotiable and immediate.
**The critical path in one line:** **The critical path in one line:**
`renew domain + make the pricing decisions → seed the Minimum Lovable Library → freeze copy → no-crypto scrub → submit to Kickstarter → launch 9/26` `assign MLL authors + D-3 co-authors → write the library → freeze copy → no-crypto scrub → submit to Kickstarter → launch 9/26`
--- ---
@@ -31,17 +30,16 @@ Before any copy or build locks, close these. Most are decisions or de-risking, n
| # | Task | Owner | Why it's first | | # | Task | Owner | Why it's first |
|:--|:--|:--|:--| |:--|:--|:--|:--|
| 0.1 | **Renew aibook.fun today.** It expires 09-05; launch is 09-26. | Whoever holds the registrar login | It's a chore, not a decision. A lapsed domain 3 weeks before launch is a self-inflicted disaster. | | 0.1 | **~~Renew aibook.fun~~ Done 2026-08-31.** | Registrar owner | Closed. |
| 0.2 | **Decide the pricing spine — lifetime *and* royalties together** (D-1 + D-2). | Commercial lead (@GTM) | Both block copy freeze; they're linked (see [Spec §6](AiBook_Spec.md)). | | 0.2 | **~~Pricing spine~~ Done 2026-08-31.** **$47/yr** is the product. No lifetime SKU as an equal. Founding writers on **contract** (v1 authored). | Commercial + Product | Copy freeze unblocked on price. |
| 0.3 | **Ratify the North-Star + product-health floors** (activation, submissions, renewal). | Product (@pat) + team | So we measure the right thing from Day 1, not just dollars. | | 0.3 | **~~Ratify the North-Star + product-health floors~~ Done 2026-08-29.** Activation 20/35 from access date; campaign waitlist 50/100; post-key submissions 10/25 in 90 days; renewal 40/60 excluding lifetime; curation is an ops SLA, not a launch bar. See PRD §8. | Product (@pat) | Locked. Don't quote 60% renewal publicly. |
| 0.4 | **Ratify the Minimum Lovable Library number and assign every workflow an author.** | Product + co-authors | This is the #1 risk; turn it from hope into owned work. | | 0.4 | **Ratify the Minimum Lovable Library number and assign every workflow an author.** | Product + co-authors | This is the #1 risk; turn it from hope into owned work. |
| 0.5 | **Confirm the two unconfirmed co-authors, or cut their verticals from v1** (D-3). | Commercial lead (@GTM) | Scope certainty before outline lock. | | 0.5 | **Confirm the two unconfirmed co-authors, or cut their verticals from v1** (D-3). | Commercial lead (@GTM) | Scope certainty before outline lock. |
| 0.6 | Open the claims register; assign a post-campaign renewal owner. | Product + Commercial | Truth-in-claims gate; don't leave renewal unstaffed. | | 0.6 | Open the claims register; assign a post-campaign renewal owner. | Product + Commercial | Truth-in-claims gate; don't leave renewal unstaffed. |
| 0.7 | Set up the Kickstarter creator account + identity verification. | Campaign lead (@THY) | Long lead time; do it early. | | 0.7 | Set up the Kickstarter creator account + identity verification. | Campaign lead (@THY) | Long lead time; do it early. |
**✅ Gate 0 — decisions closed.** Pricing spine decided · targets ratified · Minimum Lovable **✅ Gate 0 — decisions closed.** **Pricing spine signed ($47/yr; writers on contract)** · **targets ratified (2026-08-29)** · Minimum Lovable
Library scoped with authors · domain renewed · claims register open. *Until pricing is decided, Library scoped with authors · **domain renewed (2026-08-31)** · claims register open. *Until the library has named authors and D-3 is closed, don't treat content as on track. Copy freeze still waits on claims + co-author scope.*
don't freeze copy. Until the domain is renewed, don't push anything live.*
--- ---
@@ -53,8 +51,8 @@ campaign around an empty library fails.
### 1A · Content — seed the Minimum Lovable Library (critical path) ### 1A · Content — seed the Minimum Lovable Library (critical path)
| Task | Owner | | Task | Owner |
|:--|:--| |:--|:--|
| Lock the chapter outline (chapters per vertical, a named owner each) | Co-authors | | Lock the chapter outline (chapters per vertical, a named owner each). Blank is [`workflows/chapter-structure.md`](workflows/chapter-structure.md) | Co-authors |
| Publish the Workflow Recipe template + 3 filled examples | Product | | Review and freeze the chapter blank, then 3 filled examples | Product + authors |
| **Seed the Minimum Lovable Library to its floor number** (team + co-authors write & test the workflows) | Co-authors + team | | **Seed the Minimum Lovable Library to its floor number** (team + co-authors write & test the workflows) | Co-authors + team |
| 3 "clone-and-run" kits, each **verified by actually running it** | Dev + QA support | | 3 "clone-and-run" kits, each **verified by actually running it** | Dev + QA support |
| 2 sample chapters (the backer preview) | Two lead co-authors | | 2 sample chapters (the backer preview) | Two lead co-authors |
@@ -75,7 +73,7 @@ campaign around an empty library fails.
| Task | Owner | | Task | Owner |
|:--|:--| |:--|:--|
| Kickstarter page copy (all sections, 9/26 dates) | Campaign lead + exec co-author | | Kickstarter page copy (all sections, 9/26 dates) | Campaign lead + exec co-author |
| **Copy freeze — commercial terms locked** (lifetime/royalties per D-1/D-2) | Commercial lead (sign) | | **Copy freeze** (price already signed: $47/yr, writers on contract; still freeze claims + NFT scrub) | Commercial lead (sign) |
| **No-crypto scrub gate on every asset** (translation table) | Campaign lead (owner) | | **No-crypto scrub gate on every asset** (translation table) | Campaign lead (owner) |
| Hero video: script → shoot → edit (2–3 min) | Creative lead (@LFG) + co-authors | | Hero video: script → shoot → edit (2–3 min) | Creative lead (@LFG) + co-authors |
| Cover design + 3D mockups | Creative lead | | Cover design + 3D mockups | Creative lead |
+90 -36
View File
@@ -1,13 +1,13 @@
--- ---
title: "AIBook.fun — Product Requirements Document" title: "AiBook.fun — Product Requirements Document"
type: prd type: prd
status: draft status: draft
owner: Patrick (Product) owner: Patrick (Product)
launch-target: 2026-09-26 launch-target: 2026-09-26
last-updated: 2026-08-26 last-updated: 2026-08-31
--- ---
# 📚 AIBook.fun — Product Requirements Document # 📚 AiBook.fun — Product Requirements Document
> **Status: DRAFT.** Dollar figures, dates past today, and commercial terms in this > **Status: DRAFT.** Dollar figures, dates past today, and commercial terms in this
> document are planning assumptions, not commitments. Nothing here should be quoted > document are planning assumptions, not commitments. Nothing here should be quoted
@@ -20,7 +20,7 @@ last-updated: 2026-08-26
## TL;DR ## TL;DR
AIBook.fun is a **crowd-written, practical field guide to using AI agents responsibly at AiBook.fun is a **crowd-written, practical field guide to using AI agents responsibly at
work** — sold as an annual subscription, launched on Kickstarter on **September 26, 2026**. work** — sold as an annual subscription, launched on Kickstarter on **September 26, 2026**.
Its unit of content is not a chapter but a **workflow you can actually run**: a documented, Its unit of content is not a chapter but a **workflow you can actually run**: a documented,
tested recipe for getting a real job done with AI, complete with the guardrails. tested recipe for getting a real job done with AI, complete with the guardrails.
@@ -142,8 +142,9 @@ the structural answer to "how do I know this AI advice isn't hallucinated?"
| **Costs** | Model/tool costs where known — honesty about the economics | | **Costs** | Model/tool costs where known — honesty about the economics |
| **Author & sources** | Who wrote it, when, for which vertical, with citations | | **Author & sources** | Who wrote it, when, for which vertical, with citations |
**Chapters** are curated collections of these recipes, one set per vertical: Real Estate · **Chapters** are substantial essays, one per vertical (Real Estate · Financial Advisors ·
Financial Advisors · Fractional C-Suite · Higher Ed · Communities · Technology. **Editions** Fractional C-Suite · Higher Ed · Communities · Technology), with full Workflow Recipes
**nested in the argument**. See [§5.4](#54-the-reading-surface-locked-2026-08-31). **Editions**
refresh annually — which is the honest justification for a *subscription* rather than a refresh annually — which is the honest justification for a *subscription* rather than a
one-time purchase: the content genuinely goes stale as AI changes. one-time purchase: the content genuinely goes stale as AI changes.
@@ -160,15 +161,61 @@ post-launch aspiration. Community contribution is **Layer 2** — an experiment
we've seeded a library worth subscribing to. we've seeded a library worth subscribing to.
### 5.3 Be honest: v1 is an *authored* book, not yet a *crowd* book ### 5.3 Be honest: v1 is an *authored* book, not yet a *crowd* book
The prior draft's "contributor economy" pays co-authors **15% affiliate commission** — which The prior draft's "contributor economy" pays co-authors **15% affiliate commission**, which
is a reward for *referring backers*, not for *writing workflows*. Writing a workflow currently is a reward for *referring backers*, not for *writing workflows*.
earns a byline and nothing else. The mechanic that would actually pay contributors — royalties
— is unresolved (see [D-2](#10-open-decisions)).
So for v1, we state plainly: **the first edition is written by a founding set of co-authors and **Signed 2026-08-31 (D-2):** founding writers are paid **on contract**. v1 is an authored book.
the team.** Open community contribution — with a real incentive attached — is the very next Affiliate referral can still pay for referring backers. It is not how writing is paid. Open
thing we build, and its incentive design ([D-2](#10-open-decisions)) is a **launch-blocking contribution with royalties or bounties waits for Layer 2.
decision, not a nice-to-have**, because the entire "living book" thesis depends on it.
So we state plainly: **the first edition is written by a founding set of co-authors and the
team.** Layer 2 is the next thing we build, after the library is worth subscribing to.
---
### 5.4 The reading surface (locked 2026-08-31)
People read **chapters**, not a shelf of detached cards. A chapter is a substantial essay
for one vertical: it argues a way of working, then **nests full workflows inside that
argument** as the proof.
This is a deliberate break from treating the workflow as the only page. The workflow is
still the unit of *practice* (and of activation). The chapter is the unit of *reading*.
**Chapter page**
- Title, vertical, edition, authors
- Opening promise (what this chapter is for) plus a **Skip to a Try-it** control — activation
must not require finishing the essay
- Essay body: sections that teach the practice (stakes, constraints, how we work here)
- Nested **Workflow blocks** at the points in the argument they earn — not bunched in an appendix
- End matter: further workflows in this vertical, sources, last updated
**Nested workflow block** (the recipe, in full — not a teaser)
1. Chrome: recipe ID, version, time, cost
2. Title as the job
3. Problem
4. Actors
5. Setup (tools, sample files, cost)
6. **Safeguards before the steps** so data-handling is read before anyone pastes a client list
7. Method, with real tool output *in* the steps
8. Evidence
9. Try-it (~15 min, sample data, mark complete — fires Activation)
10. Share result (community template; events, not content)
11. Author, sources, last updated
**Addressability**
- Every workflow has a stable ID (e.g. `RE-01`) and a URL that opens the chapter scrolled to
that block (and a focused/print view of the same block). Campaign samples, clone-and-run
kits, and Workflow-of-the-Week link here — never to an essay with no way in.
- PDF matches this stack: chapter essay + in-place recipes. Print and web must not drift.
**Non-goals for the reader**
- No chatbot on the chapter page
- No crypto/Web3 language
- Nested recipes are not collapsed “click to reveal” cards that hide the Method
**Writing drafts** live in [`workflows/chapter-structure.md`](workflows/chapter-structure.md).
Copy that blank into a vertical file (`workflows/real-estate.md`, etc.) when a chapter is assigned.
--- ---
@@ -202,8 +249,8 @@ Requirements are grouped by the layer they serve; **most 🔴 items serve the co
| ID | Requirement | Pri | | ID | Requirement | Pri |
|:--|:--|:--| |:--|:--|:--|
| A-1 | Post-campaign access: a digital key unlocks PDF + web reader for 12 months, renews annually | 🟢 | | A-1 | Post-campaign access: a digital key unlocks PDF + web reader for 12 months, renews annually | 🟢 |
| A-2 | Anchor price **$47/yr** *(draft — needs commercial re-sign)* + hardcopy at-cost upcharge | 🔴 | | A-2 | Anchor price **$47/yr** *(signed 2026-08-31)* + hardcopy at-cost upcharge. No lifetime SKU as an equal product. | 🔴 |
| A-3 | Lifetime tier — **decision required before copy freeze** (see [D-1](#10-open-decisions)) | 🟡 | | A-3 | Lifetime tier — **not offered as a v1 SKU** (D-1 signed 2026-08-31: $47/yr only). Revisit only as a capped early-bird later, not as an equal product. | 🟢 |
| A-4 | Fulfillment ledger: backer ↔ key/upcharge tracked (a spreadsheet is fine for v1) | 🟢 | | A-4 | Fulfillment ledger: backer ↔ key/upcharge tracked (a spreadsheet is fine for v1) | 🟢 |
### 6.4 The other layers — mostly 🟡 / 🟢 ### 6.4 The other layers — mostly 🟡 / 🟢
@@ -236,7 +283,7 @@ Requirements are grouped by the layer they serve; **most 🔴 items serve the co
| NFR-3 | **Privacy:** no confidential client data in any workflow or campaign asset; track events, not content. | | NFR-3 | **Privacy:** no confidential client data in any workflow or campaign asset; track events, not content. |
| NFR-4 | **Availability:** landing page up through the campaign; emails on schedule; affiliate tracking live from Day 1 (the 30-day cookie means every hour of Day 1 counts). | | NFR-4 | **Availability:** landing page up through the campaign; emails on schedule; affiliate tracking live from Day 1 (the 30-day cookie means every hour of Day 1 counts). |
| NFR-5 | **Content quality:** every workflow passes the Recipe template + at least one co-author review. | | NFR-5 | **Content quality:** every workflow passes the Recipe template + at least one co-author review. |
| NFR-6 | **Domain continuity:** aibook.fun must not lapse (it currently expires 09-05 — see [§9](#9-immediate-next-steps)). | | NFR-6 | **Domain continuity:** aibook.fun must not lapse (**renewed 2026-08-31**). |
| NFR-7 | **Accessibility:** reader and landing page are mobile-readable, targeting WCAG AA. | | NFR-7 | **Accessibility:** reader and landing page are mobile-readable, targeting WCAG AA. |
| NFR-8 | **Cost discipline:** pre-revenue burn stays within the ~$6K budget ([Appendix C](#appendix-c--budget)). | | NFR-8 | **Cost discipline:** pre-revenue burn stays within the ~$6K budget ([Appendix C](#appendix-c--budget)). |
@@ -249,12 +296,18 @@ metrics; those measure the *campaign*, not the *product.* A funded campaign can
dead product if nobody activates, renews, or contributes. dead product if nobody activates, renews, or contributes.
### Product health (report these first) ### Product health (report these first)
| Metric | Floor | Target | Why it matters |
|:--|:--|:--|:--| **Ratified 2026-08-29 (Product).** These are the numbers we run. Do not quote the 60% renewal target on Kickstarter or any public surface.
| **Activation** — read 1 workflow + do 1 Try-it in 14 days | 20% | 35% | The North-Star. Is the core loop actually useful? |
| **Workflow submissions** during campaign window | 10 | 25 | Early signal that Layer 2 (contribution) is real | | Metric | Floor | Target | Clock / notes | Why it matters |
| **Renewal rate** — year-1 cohort | — | 60% | Is a *subscription* the right model at all? | |:--|:--|:--|:--|:--|
| **Curation turnaround** ≤ 10 working days | 80% | 95% | Can we keep quality without becoming the bottleneck? | | **Activation** — read 1 workflow + finish 1 Try-it | 20% | 35% | **14 days from access-key date, not pledge date.** North-Star. | Is the core loop actually useful? |
| **Contributor waitlist** during the campaign | 50 | 100 | People who *say* they will contribute a workflow. Not written recipes. | Early Layer-2 signal without betting launch on cold-start writing |
| **Workflow submissions** | 10 | 25 | First **90 days after keys go out** | Is paid/open contribution real once the library exists? |
| **Renewal rate** — year-1 **paid** cohort | 40% | 60% | **Exclude lifetime SKUs** from the denominator. 60% is internal. | Is a *subscription* the right model at all? |
| **Curation turnaround** ≤ 10 working days | 80% | 95% | **Ops SLA after Layer 2 is on** — not a launch health bar | Can we keep quality without becoming the bottleneck? |
Leading indicator before fulfillment: % of the pre-launch list who run the *sample* Try-it.
### Campaign performance (the fundraise) ### Campaign performance (the fundraise)
| Metric | Min | Target | Stretch | | Metric | Min | Target | Stretch |
@@ -279,13 +332,13 @@ dead product if nobody activates, renews, or contributes.
## 9. Immediate next steps ## 9. Immediate next steps
**This week — decisions and de-risking (before any copy or build locks):** **This week — decisions and de-risking (before any copy or build locks):**
1. **Renew aibook.fun today.** It expires 09-05; launch is 09-26. This is a chore, not a 1. **~~Renew aibook.fun~~ Done.** Domain renewed (confirmed 2026-08-31). Launch remains 09-26.
strategic decision — do it now and delete it from the risk list. *(Owner: whoever holds the 2. **~~Resolve the pricing spine (D-1 / D-2)~~ Done 2026-08-31.** Anchor is **$47/yr**. No
registrar login.)* lifetime SKU as an equal product. Founding writers are engaged **on contract** — v1 is an
2. **Resolve the pricing spine together — lifetime ([D-1](#10-open-decisions)) *and* royalties authored book; open royalties are not the v1 mechanic. Copy freeze is unblocked on price.
([D-2](#10-open-decisions)).** They're linked, and both block copy freeze. 3. **~~Ratify the North-Star + the product-health floors~~ Done 2026-08-29.** Locked in §8:
3. **Ratify the North-Star + the product-health floors** (§8) so we're measuring the right activation 20/35 from access date; campaign waitlist 50/100; post-key submissions 10/25 in
thing from Day 1. 90 days; renewal 40/60 excluding lifetime; curation SLA not a launch bar.
4. **Ratify the Minimum Lovable Library number and assign every workflow an author** (§5.2). 4. **Ratify the Minimum Lovable Library number and assign every workflow an author** (§5.2).
5. **Confirm the two unconfirmed co-authors** (Higher Ed, Communities) or re-scope those 5. **Confirm the two unconfirmed co-authors** (Higher Ed, Communities) or re-scope those
verticals out of v1. verticals out of v1.
@@ -313,8 +366,8 @@ decision-maker has a starting point — but the recommendation is not yet a deci
| # | Decision | Recommendation | Blocks | | # | Decision | Recommendation | Blocks |
|:--|:--|:--|:--| |:--|:--|:--|:--|
| **D-1** | **Lifetime tier — offer it, and at what price?** A ~$97–147 lifetime breaks even against $47/yr in ~2–3 years, which *undercuts the recurring-revenue thesis.* | **Pick a spine.** Keep $47/yr as the anchor. If you want a lifetime option, frame it *only* as a bounded early-bird instrument to capitalize the community fund (capped quantity), not the default SKU. Don't ship recurring + lifetime as equals. | KS copy freeze | | **D-1** | ~~Lifetime tier~~ **Signed 2026-08-31.** Product price is **$47/yr**. No lifetime SKU as an equal v1 product. | Locked. Don't quote a lifetime price. Hardcopy remains an at-cost upcharge. | — |
| **D-2** | **Contributor payment — how do people earn for *writing workflows* (not just referring)?** Today the only paid mechanic is affiliate referral. | **Resolve before launch.** This is the engine of the "living book." Decide: royalty pool, per-accepted-workflow bounty, or an explicit "v1 is authored, paid contribution ships in Q1" — but decide, don't leave blank. | The core value prop | | **D-2** | ~~Writer payment~~ **Signed 2026-08-31.** The first set of writers is engaged **on contract**. v1 is an authored book. Affiliate referral can still pay for *referring backers*; it is not how writing is paid. Open contribution + any royalty/bounty is post-v1 (Layer 2), not a launch mechanic. | Locked for v1. Revisit royalties before opening Layer 2. | — |
| **D-3** | **Co-author confirmations** (Higher Ed, Communities verticals). | Confirm within the week or cut those verticals from v1 and add post-launch. | Chapter outline, scope | | **D-3** | **Co-author confirmations** (Higher Ed, Communities verticals). | Confirm within the week or cut those verticals from v1 and add post-launch. | Chapter outline, scope |
| **D-4** | **Refresh the source-of-truth plan** to the 9/26 dates and re-signed terms (depends on D-1/D-2). | Do after D-1/D-2 land. | Copy freeze | | **D-4** | **Refresh the source-of-truth plan** to the 9/26 dates and re-signed terms (depends on D-1/D-2). | Do after D-1/D-2 land. | Copy freeze |
| **D-5** | **Human-first or agent-first for v1?** Writing for human motivation and for machine ingestion are different jobs. | **Human-first.** Build the content format so agents *can* read it later (K-7), but optimize v1 for human learners. Agent onboarding is Layer 4. | Content design | | **D-5** | **Human-first or agent-first for v1?** Writing for human motivation and for machine ingestion are different jobs. | **Human-first.** Build the content format so agents *can* read it later (K-7), but optimize v1 for human learners. Agent onboarding is Layer 4. | Content design |
@@ -375,16 +428,17 @@ changes from the prior draft, and the reasoning:
funnel are explicitly later layers. funnel are explicitly later layers.
3. **Elevated the content cold-start** to the #1 risk and added the *Minimum Lovable Library* 3. **Elevated the content cold-start** to the #1 risk and added the *Minimum Lovable Library*
launch gate (§5.2). launch gate (§5.2).
4. **Told the truth about incentives** (§5.3): v1 is an authored book; the "contributor 4. **Told the truth about incentives** (§5.3). **D-2 signed 2026-08-31:** founding writers on
economy" is currently a referral scheme; paid contribution (D-2) is now launch-blocking. contract; v1 authored; royalties wait for Layer 2. Affiliate still pays referrals, not writing.
5. **Forced the subscription-vs-lifetime tension** into a real decision (D-1). 5. **Forced the subscription-vs-lifetime tension** into a real decision. **D-1 signed 2026-08-31:**
**$47/yr** only. No lifetime SKU as an equal v1 product.
6. **Split the validation bar from the funding floor** (§8) so a funded-but-dead outcome can't 6. **Split the validation bar from the funding floor** (§8) so a funded-but-dead outcome can't
hide. hide.
7. **Reframed the pitch as utility-first** (§4, §9 hook test) — the buyer wants a workflow, not 7. **Reframed the pitch as utility-first** (§4, §9 hook test) — the buyer wants a workflow, not
an ownership philosophy. an ownership philosophy.
8. **Demoted governance to proportionate** (Appendix B), keeping only the NFT-scrub and 8. **Demoted governance to proportionate** (Appendix B), keeping only the NFT-scrub and
truth-in-claims as active gates. truth-in-claims as active gates.
9. **Turned the domain "decision" into a same-day chore** (§9). 9. **Turned the domain into a same-day chore** (§9). **Done 2026-08-31:** `aibook.fun` renewed.
10. **Translated the jargon out.** Internal codes, handles, and merge-history are gone from the 10. **Translated the jargon out.** Internal codes, handles, and merge-history are gone from the
body; a who's-who / glossary can be maintained separately if the team wants traceability body; a who's-who / glossary can be maintained separately if the team wants traceability
back to the source-of-truth IDs. back to the source-of-truth IDs.
+17 -14
View File
@@ -1,14 +1,14 @@
--- ---
title: "AIBook.fun — Product Specification" title: "AiBook.fun — Product Specification"
type: spec type: spec
status: draft status: draft
owner: Patrick (Product) owner: Patrick (Product)
launch-target: 2026-09-26 launch-target: 2026-09-26
last-updated: 2026-08-26 last-updated: 2026-08-31
companions: [AiBook_PRD.md, AiBook_Implementation_Plan.md] companions: [AiBook_PRD.md, AiBook_Implementation_Plan.md]
--- ---
# 📚 AIBook.fun — Product Specification # 📚 AiBook.fun — Product Specification
> **Status: DRAFT.** This spec is the *thinking* behind the product — the vision, the > **Status: DRAFT.** This spec is the *thinking* behind the product — the vision, the
> principles, the model. The [PRD](AiBook_PRD.md) turns it into requirements; the > principles, the model. The [PRD](AiBook_PRD.md) turns it into requirements; the
@@ -22,7 +22,7 @@ companions: [AiBook_PRD.md, AiBook_Implementation_Plan.md]
## TL;DR ## TL;DR
AIBook.fun is a **living, crowd-written field guide to using AI agents responsibly at work.** AiBook.fun is a **living, crowd-written field guide to using AI agents responsibly at work.**
Its unit of value is a **workflow you can run** — a documented, tested recipe for doing a real Its unit of value is a **workflow you can run** — a documented, tested recipe for doing a real
job with AI, including the guardrails. It launches as an annual subscription via Kickstarter on job with AI, including the guardrails. It launches as an annual subscription via Kickstarter on
**September 26, 2026**. **September 26, 2026**.
@@ -56,7 +56,8 @@ ambition — but an engine needs one cylinder firing before you add the rest. He
Everything else the product wants to be — a contributor economy, a cohort program, an Everything else the product wants to be — a contributor economy, a cohort program, an
onboarding corpus for AI agents — is a **layer built on top of this loop**, and only worth onboarding corpus for AI agents — is a **layer built on top of this loop**, and only worth
building once the loop demonstrably works. We measure that with one number, **Activation**: *did building once the loop demonstrably works. We measure that with one number, **Activation**: *did
a reader run at least one workflow within 14 days?* (Full metric set in [PRD §8](AiBook_PRD.md).) a reader run at least one workflow and finish one Try-it within 14 days of getting access
(access key, not pledge)?* Floors/targets ratified 2026-08-29 — full set in [PRD §8](AiBook_PRD.md).
### The product in layers ### The product in layers
| Layer | What | When | | Layer | What | When |
@@ -95,7 +96,7 @@ These are gates, not preferences — a workflow or a claim either clears them or
| Actor | Who | Their job | The boundary | | Actor | Who | Their job | The boundary |
|:--|:--|:--|:--| |:--|:--|:--|:--|
| **Reader / subscriber** | Practitioners (real estate, financial advisors, fractional execs, higher ed, communities, tech) | Get current, trustworthy AI workflows to apply this week | Pays for access (~$47/yr — *draft*) | | **Reader / subscriber** | Practitioners (real estate, financial advisors, fractional execs, higher ed, communities, tech) | Get current, trustworthy AI workflows to apply this week | Pays for access (**$47/yr** — signed) |
| **Co-author** | Confirmed: three founders across RE/FA, C-suite, and tech. Potential: two more for Higher Ed & Communities | Own a vertical; write and curate its workflows | Content follows the template; curated | | **Co-author** | Confirmed: three founders across RE/FA, C-suite, and tech. Potential: two more for Higher Ed & Communities | Own a vertical; write and curate its workflows | Content follows the template; curated |
| **Curator / editor** | Co-authors + community editors | Keep the quality bar; run the editorial pipeline | Curating ≠ owning the platform | | **Curator / editor** | Co-authors + community editors | Keep the quality bar; run the editorial pipeline | Curating ≠ owning the platform |
| **Affiliate** | Community, podcast, press | Refer backers; earn commission | Commission rates below | | **Affiliate** | Community, podcast, press | Refer backers; earn commission | Commission rates below |
@@ -146,9 +147,11 @@ answer to "how do I know this isn't hallucinated AI advice?"
| **Costs** | Model/tool costs where known — honesty about economics | | **Costs** | Model/tool costs where known — honesty about economics |
| **Author & sources** | Who, when, which vertical, with citations | | **Author & sources** | Who, when, which vertical, with citations |
**Chapters** are curated collections of these per vertical. **Editions** refresh annually — **Chapters** are the reading unit: a substantial essay per vertical, with **full workflows
nested in the argument** (not a table of contents of cards). **Editions** refresh annually —
which is the honest justification for a *subscription*: AI moves fast enough that a static book which is the honest justification for a *subscription*: AI moves fast enough that a static book
goes stale, so the value is in staying current. goes stale, so the value is in staying current. Reading IA locked 2026-08-31; see
[PRD §5.4](AiBook_PRD.md).
### 4.2 Seed the shelf before you sell tickets (the cold-start rule) ### 4.2 Seed the shelf before you sell tickets (the cold-start rule)
The value proposition is a rich library. The failure mode is a near-empty one. Crowd-sourced The value proposition is a rich library. The failure mode is a near-empty one. Crowd-sourced
@@ -179,7 +182,7 @@ around and after launch, not gating it:
### 4.4 Digital surface (current state) ### 4.4 Digital surface (current state)
| Surface | What it is | Status | | Surface | What it is | Status |
|:--|:--|:--| |:--|:--|:--|
| `aibook.fun` domain | Public home: pitch, sample workflows, affiliate signup, campaign link | Parked — **expires 09-05, renew immediately** | | `aibook.fun` domain | Public home: pitch, sample workflows, affiliate signup, campaign link | **Renewed 2026-08-31** |
| Landing page (built) | A staged proof-of-concept, ready to review and push live | Staged locally, not yet public | | Landing page (built) | A staged proof-of-concept, ready to review and push live | Staged locally, not yet public |
| Subscription access | Digital key → PDF + web reader + annual updates | Not built yet (post-campaign) | | Subscription access | Digital key → PDF + web reader + annual updates | Not built yet (post-campaign) |
| Community | Team channel live; cohort spaces not built | Partial | | Community | Team channel live; cohort spaces not built | Partial |
@@ -218,9 +221,9 @@ from the core loop.*
| Item | Value | Status | | Item | Value | Status |
|:--|:--|:--| |:--|:--|:--|
| Anchor subscription | $47/yr digital + hardcopy upcharge | **Draft** — needs commercial re-sign for the 9/26 relaunch | | Anchor subscription | **$47/yr** digital + hardcopy at-cost upcharge | **Signed 2026-08-31** |
| Lifetime access | Proposed; ~$97–147 one-time *(figures illustrative — do not quote)* | **Open decision** ([D-1](#8-open-decisions)) | | Lifetime access | Not a v1 SKU | **Signed D-1:** $47/yr only |
| Contributor royalties | None documented today; current mechanic is affiliate referral only | **Open decision** ([D-2](#8-open-decisions)) | | Founding writers | Paid **on contract** (v1 is authored). Affiliate still pays referrals, not writing. | **Signed D-2 2026-08-31.** Royalties/bounties wait for Layer 2. |
| Projected revenue | ~$53,600 at 706 backers | Draft planning figure | | Projected revenue | ~$53,600 at 706 backers | Draft planning figure |
| Pre-revenue budget | ~$6,000 (ads + KS fees + affiliate fee) | Draft planning figure | | Pre-revenue budget | ~$6,000 (ads + KS fees + affiliate fee) | Draft planning figure |
| "eBookNFT platform" | A future idea; this book is the first product | Not in scope | | "eBookNFT platform" | A future idea; this book is the first product | Not in scope |
@@ -274,8 +277,8 @@ carries a recommendation as a starting point, not a settled answer.
| # | Decision | Recommendation | | # | Decision | Recommendation |
|:--|:--|:--| |:--|:--|:--|
| **D-1** | Lifetime tier — offer it, at what price/position? | Keep $47/yr as the anchor; a lifetime option only as a bounded early-bird capitalization play, not the default. | | **D-1** | ~~Lifetime~~ **Signed 2026-08-31:** **$47/yr** only. No lifetime SKU as an equal product. | Locked. |
| **D-2** | Contributor payment — how do people earn for *writing* workflows, not just referring? | **Resolve before launch** — it's the engine of the "living book." Royalty pool, per-workflow bounty, or an explicit "v1 authored, paid contribution in Q1." | | **D-2** | ~~Writer pay~~ **Signed 2026-08-31:** founding writers on **contract**. v1 authored. Layer 2 royalties later. | Locked for v1. |
| **D-3** | Confirm the two unconfirmed co-authors (Higher Ed, Communities) | Confirm this week, or cut those verticals from v1 and add post-launch. | | **D-3** | Confirm the two unconfirmed co-authors (Higher Ed, Communities) | Confirm this week, or cut those verticals from v1 and add post-launch. |
| **D-4** | Refresh the source-of-truth plan to the 9/26 dates & re-signed terms | Do after D-1/D-2 land. | | **D-4** | Refresh the source-of-truth plan to the 9/26 dates & re-signed terms | Do after D-1/D-2 land. |
| **D-5** | Human-first or agent-first for v1? | Human-first. Build the format so agents *can* read it later; optimize v1 for human learners. | | **D-5** | Human-first or agent-first for v1? | Human-first. Build the format so agents *can* read it later; optimize v1 for human learners. |
+18 -17
View File
@@ -1,15 +1,15 @@
# 📚 AIBook.fun # 📚 AiBook.fun
**A living, crowd-written field guide to using AI agents responsibly at work.** **A living, crowd-written field guide to using AI agents responsibly at work.**
AIBook.fun's unit of content isn't a chapter — it's a **workflow you can actually run**: a People read **chapters**. Each chapter is a substantial essay for one vertical, with full
documented, tested recipe for getting a real job done with AI, guardrails included. It's written workflows nested in the argument. The workflow is the unit of practice: a documented, tested
and kept current by the practitioners who use it, and it launches as an annual subscription, recipe for a real job, guardrails included. The first edition is written on contract by founding
crowdfunded on Kickstarter. authors. It launches as a **$47/yr** subscription, crowdfunded on Kickstarter.
> **Status: planning stage.** This repository holds the product's planning documents. The product > **Status: planning + writing.** Planning docs and the chapter blank live here. The product is
> is not yet live, and everything here is a working draft — figures, dates, and commercial terms > not yet live. **$47/yr** is signed. Domain `aibook.fun` is renewed. Other figures and unsigned
> are planning assumptions, not commitments. Public launch is **targeted for September 26, 2026**. > terms are still planning assumptions, not for public quote. Launch target **26 Sep 2026**.
## The idea in one loop ## The idea in one loop
@@ -30,22 +30,23 @@ not read AI theory.
## What's in this repository ## What's in this repository
This is a documentation and planning repo. Three companion documents define the product: This is a documentation and writing repo.
| Document | What it covers | | Document | What it covers |
|:--|:--| |:--|:--|
| [Product Requirements (PRD)](AiBook_PRD.md) | What we're building and why — scope, requirements, success metrics, open decisions | | [Product Requirements (PRD)](AiBook_PRD.md) | What we're building and why. Scope, requirements, success metrics, open decisions |
| [Product Specification (Spec)](AiBook_Spec.md) | The thinking behind it — vision, principles, content model, business model | | [Product Specification (Spec)](AiBook_Spec.md) | The thinking behind it. Vision, principles, content model, business model |
| [Implementation Plan](AiBook_Implementation_Plan.md) | How and when — the path from now to launch and fulfillment | | [Implementation Plan](AiBook_Implementation_Plan.md) | How and when. Path from now to launch and fulfillment |
| [Chapter structure](workflows/chapter-structure.md) | The blank chapter for review. Copy it into a vertical file when writing starts |
New here? Start with the **[Spec](AiBook_Spec.md)** for the vision, then the **[PRD](AiBook_PRD.md)** New here? Start with the **[Spec](AiBook_Spec.md)** for the vision, then the **[PRD](AiBook_PRD.md)**
for the specifics. for the specifics. Writers start at **[chapter structure](workflows/chapter-structure.md)**.
## How it works (the short version) ## How it works (the short version)
- **Content:** every entry follows one *Workflow Recipe* template — problem, method (showing the - **Content:** readers get a chapter essay. Nested in it, every workflow follows one recipe:
real tool output), safeguards, evidence, a ~15-minute Try-it, and costs. The template is the problem, actors, setup, safeguards, method (real tool output), evidence, a ~15-minute Try-it,
quality guarantee — the structural answer to "how do I know this AI advice isn't made up?" share, costs, author. Safeguards sit before the steps. The template is the quality guarantee.
- **Editions:** the guide refreshes annually — which is why it's a subscription. AI moves fast, - **Editions:** the guide refreshes annually — which is why it's a subscription. AI moves fast,
and stale advice is worthless. and stale advice is worthless.
- **Contribution:** the first edition is written by a founding set of co-authors; open community - **Contribution:** the first edition is written by a founding set of co-authors; open community
@@ -61,7 +62,7 @@ for the specifics.
| Post-launch | Fulfill the first edition; open community contribution | | Post-launch | Fulfill the first edition; open community contribution |
| Later | Cohort learning, annual editions, deeper ecosystem integration | | Later | Cohort learning, annual editions, deeper ecosystem integration |
AIBook.fun is part of the **WeOwnNet** ecosystem. AiBook.fun is part of the **WeOwnNet** ecosystem.
## License ## License
+73
View File
@@ -0,0 +1,73 @@
# aibook-fun deploy — gitignore
# ⚠️ CRITICAL: Never commit credentials or state files
# =============================================================================
# Terraform / OpenTofu State (CRITICAL - contains infrastructure details)
# =============================================================================
*.tfstate
*.tfstate.*
*.tfstate.backup
.terraform/
# NOTE: .terraform.lock.hcl is NOT ignored — commit it so provider versions
# are reproducible across machines/CI runs.
# Terraform variable files with real values
terraform.tfvars
terraform.tfvars.json
*.auto.tfvars
*.auto.tfvars.json
# Terraform crash logs
crash.log
crash.*.log
# =============================================================================
# Environment Files (CRITICAL - contains passwords)
# =============================================================================
.env
.env.prod
.env.local
.env.*.local
# =============================================================================
# Backups (may contain sensitive data)
# =============================================================================
backups/
*.tar.gz
*.sql
*.sql.gz
# =============================================================================
# Logs
# =============================================================================
logs/
*.log
# =============================================================================
# OS & Editor
# =============================================================================
.DS_Store
.DS_Store?
._*
Thumbs.db
*.swp
*.swo
*~
.idea/
.vscode/
*.code-workspace
# =============================================================================
# Docker
# =============================================================================
docker-compose.override.yml
docker-compose.override.yaml
# =============================================================================
# Ansible
# =============================================================================
*.retry
.vault-pass
# OpenTofu saved plans (sensitive) — never commit
*.tfplan
+25
View File
@@ -0,0 +1,25 @@
# Deploy
Provisions a DO droplet + reserved IP + firewall + monitoring, builds the
Next.js image locally on the droplet, runs it behind Caddy.
## Deploy from scratch
```bash
cd terraform
infisical login
export INFISICAL_PROJECT_ID=<aibook-fun-project-id>
./itofu.sh init && ./itofu.sh plan && ./itofu.sh apply
./itofu.sh output -raw droplet_ip
# add Cloudflare A record: aibook.fun → <that-ip>
cd ..
ansible-playbook -i 'root@<that-ip>,' ansible/deploy.yml
```
## Structure
- `terraform/` — droplet + IP + firewall + monitoring
- `docker/` — Dockerfile + compose + Caddy
- `ansible/` — droplet convergence
+33
View File
@@ -0,0 +1,33 @@
# Ansible settings for deploying aibook-fun.
#
# The whole point of this file is SSH connection reuse. On a workstation whose
# SSH key lives in a hardware-backed agent (Secretive, a YubiKey, the Secure
# Enclave), every *new* TCP connection needs a fresh signature — which means a
# Touch ID prompt. Ansible opens many connections per run and does so
# non-interactively, so those prompts cannot be answered and the run dies with:
#
# sign_and_send_pubkey: signing failed ... agent refused operation
#
# Ansible's own defaults make this worse in two ways, both fixed below:
#
# 1. It keeps control sockets in ~/.ansible/cp, separate from the ones the
# operator's own `ssh` has already authenticated. Pointing it at the same
# directory and the same %C naming as OpenSSH means ansible *reuses* a
# master connection the operator authorised earlier — no new signature.
# 2. ControlPersist defaults to 60s, so a master closes between steps of a
# deploy and the next one needs another signature.
#
# Together: authenticate once at the start of a work session, then deploy
# repeatedly without touching the key again.
[defaults]
host_key_checking = True
interpreter_python = auto_silent
[ssh_connection]
# Match OpenSSH's own ControlPath so ansible shares the operator's masters.
# %%C is escaped for configparser — ssh receives %C (a hash of local host,
# remote host, port and user), exactly what ~/.ssh/config uses.
control_path_dir = ~/.ssh/sockets
control_path = %(directory)s/%%C
ssh_args = -C -o ControlMaster=auto -o ControlPersist=4h
pipelining = True
+150
View File
@@ -0,0 +1,150 @@
---
# Usage: ansible-playbook -i 'root@<droplet-ip>,' deploy/ansible/deploy.yml
# Pre: droplet cloud-init complete (.bootstrap-complete marker present)
- name: Deploy aibook-fun app layer
hosts: all
gather_facts: false
become: true
vars:
app_dir: /opt/aibook_fun
repo_dir: /opt/aibook_fun/repo
repo_url: "https://git.weown.tools/WeOwnAI/aibook-fun.git"
repo_branch: main
image_tag: "aibook-fun-marketing:v1"
pre_tasks:
- name: Verify droplet has completed cloud-init bootstrap
ansible.builtin.stat:
path: "{{ app_dir }}/.bootstrap-complete"
register: bootstrap_marker
- name: Fail clearly if bootstrap is not complete
ansible.builtin.fail:
msg: >-
{{ app_dir }}/.bootstrap-complete is missing — droplet has not
finished cloud-init. Wait a few minutes after `tofu apply` and retry.
Check /var/log/cloud-init-output.log on the droplet.
when: not bootstrap_marker.stat.exists
- name: Ensure app dir exists
ansible.builtin.file:
path: "{{ app_dir }}"
state: directory
owner: root
group: root
mode: "0755"
tasks:
# ─── Clone / update repo on droplet ────────────────────────────────────
- name: Clone (or update) aibook-fun repo on droplet
ansible.builtin.git:
repo: "{{ repo_url }}"
dest: "{{ repo_dir }}"
version: "{{ repo_branch }}"
force: true
register: git_clone
notify: Rebuild image
# ─── Upload Dockerfile (deploy/ scaffold not yet pushed to git.weown.tools) ─
- name: Upload Dockerfile to droplet
ansible.builtin.copy:
src: ../docker/Dockerfile
dest: "{{ app_dir }}/Dockerfile"
owner: root
group: root
mode: "0644"
# ─── Build docker image locally on droplet (unconditional — docker layer cache handles no-ops) ─
- name: Build Next.js docker image locally
ansible.builtin.shell: |
set -euo pipefail
docker build -t {{ image_tag }} -f {{ app_dir }}/Dockerfile {{ repo_dir }}/marketing-site/
args:
executable: /bin/bash
register: image_build
changed_when: "'writing image' in image_build.stderr"
notify: Reconcile docker compose stack
# ─── Upload compose + Caddyfile ────────────────────────────────────────
- name: Upload compose.yaml
ansible.builtin.copy:
src: ../docker/compose.prod.yaml
dest: "{{ app_dir }}/compose.yaml"
owner: root
group: root
mode: "0644"
notify: Reconcile docker compose stack
- name: Upload Caddyfile
ansible.builtin.copy:
src: ../docker/Caddyfile
dest: "{{ app_dir }}/Caddyfile"
owner: root
group: root
mode: "0644"
notify: Reconcile docker compose stack
- name: Ensure caddy log dir is writable
ansible.builtin.file:
path: /var/log/caddy
state: directory
owner: root
group: root
mode: "0777"
- name: Ensure Caddy Docker volumes are writable (caddy runs as UID 1000)
ansible.builtin.shell: |
set -euo pipefail
for suffix in caddy_data caddy_config; do
path="/var/lib/docker/volumes/aibook_fun_${suffix}/_data"
if [ -d "$path" ]; then
chown -R 1000:1000 "$path"
fi
done
args:
executable: /bin/bash
changed_when: false
# ─── Ensure compose stack is running (idempotent) ──────────────────────
- name: Ensure compose stack is running (idempotent — `up -d` no-op if unchanged)
ansible.builtin.shell: |
set -euo pipefail
cd {{ app_dir }}
docker compose up -d --remove-orphans
args:
executable: /bin/bash
register: compose_up
changed_when: "'Started' in compose_up.stderr or 'Created' in compose_up.stderr or 'Recreated' in compose_up.stderr"
- name: Wait for web app to respond
ansible.builtin.uri:
url: http://127.0.0.1:3000/
status_code: [200]
register: health_probe
retries: 20
delay: 5
until: health_probe.status == 200
- name: Report success
ansible.builtin.debug:
msg: "aibook-fun app layer deployed. URL: https://aibook.fun"
handlers:
- name: Rebuild image
ansible.builtin.shell: |
set -euo pipefail
cd {{ repo_dir }}
docker build -t {{ image_tag }} -f {{ repo_dir }}/deploy/docker/Dockerfile {{ repo_dir }}/marketing-site/
args:
executable: /bin/bash
changed_when: false
- name: Reconcile docker compose stack
ansible.builtin.shell: |
set -euo pipefail
cd {{ app_dir }}
docker compose up -d --force-recreate --remove-orphans
args:
executable: /bin/bash
changed_when: false
+76
View File
@@ -0,0 +1,76 @@
# aibook-fun — host hardening (CIS-aligned, Phase A)
#
# Applies the DevSec hardening baseline (≈ CIS Ubuntu L1) to the droplet:
# kernel/sysctl, auth policy, filesystem perms, SSH crypto + policy — then
# measures the result with Lynis. Idempotent; run any time after bootstrap:
#
# ansible-galaxy collection install -r ansible/requirements.yml # once
# ansible-playbook ansible/harden.yml -i 'root@<droplet-ip>,'
#
# PHASE A vs PHASE B (deliberate scope — do not "fix" this into Phase B):
# Phase A (this play): CIS L1 OS + SSH hardening with root login KEPT
# (key-only). Every fleet tool (deploy.sh, backup.sh, manage-droplets.sh)
# SSHes as root@; removing root here would break the fleet mid-flight.
# Phase B (tracked, pre-first-customer): ops-user model (root SSH off,
# password-sudo 2FA), mesh/CIDR-locked port 22 — per SOP-INFRA-012. Needs
# the fleet tooling migrated off root@ first.
#
# Docker-safe exceptions (the CIS-L2-vs-container-runtime conflicts):
# - net.ipv4.ip_forward stays 1 — Docker bridge networking requires it. A
# defaults-level override silently LOSES to the inner role's defaults;
# it must be a task-var on the include (hit live 2026-07-03: containers
# got DNS but no TCP egress when this landed at 0).
---
- name: Harden aibook-fun host (CIS L1 via devsec — Phase A)
hosts: all
become: true
vars:
run_lynis: true
tasks:
- name: "OS hardening (devsec.hardening.os_hardening ≈ CIS Ubuntu L1)"
ansible.builtin.include_role:
name: devsec.hardening.os_hardening
vars:
# MUST be task-vars here (see header) — Docker bridge needs ip_forward.
sysctl_overwrite:
net.ipv4.ip_forward: 1
- name: "SSH hardening (devsec.hardening.ssh_hardening — key-only, strong KEX/ciphers)"
ansible.builtin.include_role:
name: devsec.hardening.ssh_hardening
vars:
# Phase A: root stays (fleet tooling), but key-only and CIS crypto.
ssh_permit_root_login: "without-password"
ssh_password_authentication: false
ssh_challenge_response_authentication: false
- name: "Measure: install + run Lynis (hardening index; non-blocking)"
when: run_lynis | bool
block:
- name: "Install Lynis"
ansible.builtin.apt:
name: lynis
state: present
update_cache: true
- name: "Run Lynis audit (quick)"
ansible.builtin.command: lynis audit system --quick --no-colors
register: lynis_out
changed_when: false
failed_when: false
- name: "Lynis summary (record this index in the site CHANGELOG)"
ansible.builtin.debug:
msg: "{{ lynis_out.stdout_lines | select('search', 'Hardening index|Warnings|Suggestions|Tests performed') | list }}"
- name: "Verify Docker survived hardening (containers must still run)"
# brace-free format string: Go-template braces here would be re-templated
# by ansible's own Jinja pass and error out.
ansible.builtin.command: docker info --format json
register: docker_ok
changed_when: false
- name: "Docker status"
ansible.builtin.debug:
msg: "Docker OK (server {{ (docker_ok.stdout | from_json).ServerVersion | default('unknown') }}) — hardening did not break the runtime"
+8
View File
@@ -0,0 +1,8 @@
# Ansible Galaxy requirements for the optional hardening play (harden.yml).
# Install once per operator workstation:
# ansible-galaxy collection install -r ansible/requirements.yml
collections:
# DevSec Hardening Framework — CIS/STIG-aligned os_hardening + ssh_hardening.
# https://github.com/dev-sec/ansible-collection-hardening
- name: devsec.hardening
version: ">=8.8.0"
+23
View File
@@ -0,0 +1,23 @@
aibook.fun {
# AIBook.fun is a static marketing site — pure reverse proxy to the Next.js
# standalone server. No auth/headers added here beyond baseline security.
reverse_proxy web:3000
log {
# Host-bind-mounted /var/log/caddy/ so logs survive container recreation.
output file /var/log/caddy/aibook-fun.log
}
header {
# Force HTTPS for a year (HSTS — a lapsed cert is a service outage, not a
# warning; weown rail per W24 phase4 findings).
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
X-XSS-Protection "1; mode=block"
Referrer-Policy strict-origin-when-cross-origin
-Server
}
}
+41
View File
@@ -0,0 +1,41 @@
# syntax=docker/dockerfile:1
# aibook-fun — Next.js 16 marketing site (marketing-site/), standalone output.
#
# Build context = marketing-site/ (see .gitea/workflows/deploy.yml):
# docker build -f deploy/docker/Dockerfile -t <image> marketing-site/
#
# Multi-stage: deps → build (next build → .next/standalone) → slim non-root runner.
# Requires `output: 'standalone'` in next.config.mjs (added on this branch).
# Node 20.9+ / 22+ required by Next 16; pin 22 LTS.
FROM node:22-alpine AS deps
WORKDIR /app
# Lockfiles first for layer caching.
COPY package.json package-lock.json ./
RUN npm ci
FROM node:22-alpine AS builder
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Next standalone ships as a single node server.js; run as non-root (uid 1001).
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 --ingroup nodejs nextjs
# Static assets not bundled into the standalone output.
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
CMD ["node", "server.js"]
+59
View File
@@ -0,0 +1,59 @@
services:
web:
image: aibook-fun-marketing:v1
pull_policy: never
restart: unless-stopped
ports:
# Loopback-only: lets caddy proxy and health probes reach the app.
# Not internet-exposed; caddy handles :80/:443 → web:3000.
- "127.0.0.1:3000:3000"
networks:
- aibooknet
healthcheck:
test: ["CMD-SHELL", "curl -f -o /dev/null -w '%{http_code}' http://localhost:3000/ | grep -q '^200$' || exit 1"]
start_period: 30s
interval: 30s
timeout: 10s
retries: 5
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 128M
caddy:
image: caddy:2
restart: unless-stopped
depends_on:
- web
environment:
DOMAIN: aibook.fun
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
- /var/log/caddy:/var/log/caddy
networks:
- aibooknet
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 128M
volumes:
caddy_data:
name: aibook_fun_caddy_data
caddy_config:
name: aibook_fun_caddy_config
networks:
aibooknet:
name: aibook_fun_net
driver: bridge
+41
View File
@@ -0,0 +1,41 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/digitalocean/digitalocean" {
version = "2.100.0"
constraints = "~> 2.36"
hashes = [
"h1:+1p5n5KeGjz/ulw/ZLXoaBkF+2UTgKqWqERw1nA2sJQ=",
"h1:79G5uDt1VHL6gWtSnK8lCOHZpo/jp/kSGA7FXtqQvX4=",
"h1:7r9AZrpc8pB6PRsJfmxSGLzIRmohmfpX6RrnA1FLbh4=",
"h1:9VsRnY7rmGiMFVG4ix3ry2fx8B8d7l9PyEpmGjjiW3I=",
"h1:LchVh6xhpXw1PEnl2mRnsLlR4dzLePw8M2vw4DuPk9U=",
"h1:OmnMLcKMFnZLI1CvTgQBod0awOpuVxsfB8dqBejfwE8=",
"h1:Q9BxzK1Tp8YeuCUXVogVfqTsVCrWUUJJdvpL7UA0W/s=",
"h1:R13HFu3vyxrd4aHyh/+p2YY0vWpPAFjafBRYVuIL5HE=",
"h1:RJH5t57bLx1b2+l+gS6RRHyakhEQjSKL/QYu6npCtbI=",
"h1:WQ2ClvMwBILHDzWWhAm3LBRra4HkSe9taUlUGSLBNWI=",
"h1:WbQfCVfC+GU0qPSoZKuoEllr3cMqwTC1Fx5DfbUjy/o=",
"h1:iwjK8zLBTHWHICCP/O+QFcyxH16bV4gKPFUbmu2foqc=",
"h1:pTFXceDc5intkLpVFVW3PKfwkF1CvmnWi3vYcF6SAQs=",
"h1:pc1iMmHPQA37PCkVWTRzuO/WgaoSGYtSBVP5paO6nK4=",
"h1:uDVCzJGtpV2DqyPWoYWjQpbEV48PxS6/U7/uiGfDDsU=",
"h1:yyBAhHZmejmdvjuvsF2gAwIZebqOmisQIT5dOQqQx9A=",
"zh:021dbdacf7dd636cb66990964737bc4a6402160b5301e06b6fdab03a814f7e96",
"zh:083ff822b22738eedc9eae29aa5d4c369b2de921e49fefa9858a4e7c6274b0c1",
"zh:10695b5f7dbbe8ba1a20ac6bea1ab8613b43d2b1f6f9b53369bed296b2dff4d2",
"zh:110124b9d07cd1f0f2831ad27dfeb5a010a070952ea9a9e675ca3cfe158a0ffa",
"zh:409a8b22ffb9a53caf57d0a16f5f8ac0a2fcd61b6b0bcd512a685ea26cd06047",
"zh:488e4b494c198c0696c85d2c73c66c40567ca2ad5230bbd95d8ffdead86e646d",
"zh:4dd5b6e8c1be5fb619812fe738d0b49e0d24731fb290c0ab5190b7bc60c70e66",
"zh:6717fa7f623ad4c9b4d99acceffd1372d272ff02d7c98259dd17ec7ef11b836c",
"zh:7a25445caebd2e7f6ab2381578ecaf31744159a7e4a16476f19112d2e50d8519",
"zh:86b3d7050ba04ec7e0a14dbee8c0540fca8f4377fb072f87c88a6fcd26388714",
"zh:c23f30a38930c283055b6f04acbcad06cb0dd3c71c172eb23bb239f50dd12823",
"zh:d1f9c5be7f7d5c71830357bdd9c0fb529185e9380092e1b792f969495c943790",
"zh:d222573e7de8f9ad1c5b87a6c44c2dc59947c3968b3e70b7d4c76263d6c8fe5c",
"zh:e1f2d606661435416c4d2b48db80244d0acef88a5ce81b5dcbd65ff2642b1b4d",
"zh:ea13fad19414586864557601c59e85188601e150c815ec1e71f8ed83f1942a50",
"zh:f795be71dc933aaec7145f93068da17f227783964d33a79de591a042aac7c3ca",
]
}
+25
View File
@@ -0,0 +1,25 @@
# aibook-fun - Terraform Backend Configuration
# State stored in DigitalOcean Spaces (S3-compatible) for team sharing, SSE-C
# encrypted at rest.
#
# Pass credentials via itofu.sh (preferred) or -backend-config flags:
# access_key / secret_key / sse_customer_key
# Backend config cannot reference Terraform variables — itofu.sh forwards them.
terraform {
backend "s3" {
endpoint = "https://atl1.digitaloceanspaces.com"
bucket = "weown-tools-state"
key = "aibook-fun/aibook-fun.tfstate"
region = "us-east-1"
encrypt = true
acl = "private"
# Required for DO Spaces (S3-compatible but not AWS)
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
skip_region_validation = true
skip_s3_checksum = true
}
}
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Run OpenTofu with TF_VAR_* creds injected from Infisical /tofu/.
# Usage: infisical login && INFISICAL_PROJECT_ID=<id> ./itofu.sh <init|plan|apply|...>
set -euo pipefail
INFISICAL_PROJECT_ID="${INFISICAL_PROJECT_ID:-}"
INFISICAL_ENV="${INFISICAL_ENV:-prod}"
TOFU_PATH="/tofu"
PLAN_FILE="${WEOWN_TOFU_PLAN:-plan.tfplan}"
: "${INFISICAL_PROJECT_ID:?INFISICAL_PROJECT_ID not set. Export it (e.g. from site.conf) before running itofu.sh.}"
command -v infisical >/dev/null 2>&1 || { echo "ERROR: infisical CLI not found." >&2; exit 1; }
command -v tofu >/dev/null 2>&1 || { echo "ERROR: tofu not found." >&2; exit 1; }
infisical export --projectId="$INFISICAL_PROJECT_ID" --env="$INFISICAL_ENV" --path="$TOFU_PATH" >/dev/null 2>&1 || {
echo "ERROR: cannot read $INFISICAL_PROJECT_ID $INFISICAL_ENV $TOFU_PATH — run 'infisical login' and check your access." >&2; exit 1; }
[ "$#" -ge 1 ] || { echo "usage: ./itofu.sh <init|plan|apply|output|destroy|...> [args]" >&2; exit 1; }
irun() { infisical run --projectId="$INFISICAL_PROJECT_ID" --env="$INFISICAL_ENV" --path="$TOFU_PATH" -- "$@"; }
# S3 backend reads AWS_* env on every state op — so ALL tofu calls go through trun.
# shellcheck disable=SC2016
trun() { irun bash -c 'export AWS_ACCESS_KEY_ID="$TF_VAR_spaces_access_key" AWS_SECRET_ACCESS_KEY="$TF_VAR_spaces_secret_key"; exec tofu "$@"' itofu "$@"; }
case "$1" in
init)
shift
# shellcheck disable=SC2016
irun bash -c 'export AWS_ACCESS_KEY_ID="$TF_VAR_spaces_access_key" AWS_SECRET_ACCESS_KEY="$TF_VAR_spaces_secret_key"; exec tofu init -reconfigure \
-backend-config="sse_customer_key=$TF_VAR_spaces_encryption_key" "$@"' itofu "$@"
;;
plan)
shift
trun plan -out="$PLAN_FILE" "$@"
;;
apply)
shift
if [ "$#" -eq 0 ] && [ -f "$PLAN_FILE" ]; then
set +e; trun apply "$PLAN_FILE"; rc=$?; set -e
rm -f "$PLAN_FILE"
exit "$rc"
fi
trun apply "$@"
;;
*)
trun "$@"
;;
esac
+113
View File
@@ -0,0 +1,113 @@
# aibook-fun - Main Infrastructure
# Managed by OpenTofu
resource "digitalocean_droplet" "web" {
name = "aibook-fun"
image = var.droplet_image
size = var.droplet_size
region = var.region
monitoring = true
backups = true
# CPU/RAM-only resize (provider default is true = also grow the disk).
resize_disk = false
ssh_keys = var.ssh_key_fingerprints
user_data = templatefile("${path.module}/templates/cloud-init.yaml", {
project_name = "aibook_fun"
})
tags = ["aibook-fun", "web", "weown-ai"]
lifecycle {
# A droplet destroy means losing every Docker volume + cert state on it.
# make any such plan a hard error instead of an accident.
prevent_destroy = true
# user_data/tags/ssh_keys are create-time-only on DO; ignore drift so
# unrelated changes don't arm `# forces replacement` on next apply.
ignore_changes = [user_data, tags, ssh_keys]
}
}
# Optional encrypted-at-rest data volume (default 0 = stateless: droplet local
# disk only). Enable by setting data_volume_size_gb > 0 if the site ever needs
# durable state; DO block storage is encrypted at rest, the droplet disk is not.
resource "digitalocean_volume" "data" {
count = var.data_volume_size_gb > 0 ? 1 : 0
region = var.region
name = "aibook-fun-data"
size = var.data_volume_size_gb
initial_filesystem_type = "ext4"
description = "aibook-fun encrypted-at-rest data (docker data-root)"
lifecycle {
prevent_destroy = true
}
}
resource "digitalocean_volume_attachment" "data" {
count = var.data_volume_size_gb > 0 ? 1 : 0
droplet_id = digitalocean_droplet.web.id
volume_id = digitalocean_volume.data[0].id
}
resource "digitalocean_reserved_ip" "web" {
region = var.region
}
resource "digitalocean_reserved_ip_assignment" "web" {
ip_address = digitalocean_reserved_ip.web.ip_address
droplet_id = digitalocean_droplet.web.id
}
#trivy:ignore:AVD-DIG-0001 # Public web server: HTTP/HTTPS inbound from internet is required by design
#trivy:ignore:AVD-DIG-0003 # Public web server: unrestricted outbound required for package installs, ACME, image pulls
resource "digitalocean_firewall" "web" {
name = "aibook-fun-fw"
droplet_ids = [digitalocean_droplet.web.id]
# SSH — restrict via var.ssh_source_cidrs. ⚠️ PRODUCTION: pin to admin IP/32 or VPN
inbound_rule {
protocol = "tcp"
port_range = "22"
source_addresses = var.ssh_source_cidrs
}
# HTTP (ACME challenge + redirect)
inbound_rule {
protocol = "tcp"
port_range = "80"
source_addresses = ["0.0.0.0/0", "::/0"]
}
# HTTPS
inbound_rule {
protocol = "tcp"
port_range = "443"
source_addresses = ["0.0.0.0/0", "::/0"]
}
# HTTPS/QUIC (HTTP/3)
inbound_rule {
protocol = "udp"
port_range = "443"
source_addresses = ["0.0.0.0/0", "::/0"]
}
# All outbound TCP (apt, ACME, container image pulls)
outbound_rule {
protocol = "tcp"
port_range = "1-65535"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
# All outbound UDP (DNS, NTP, QUIC)
outbound_rule {
protocol = "udp"
port_range = "1-65535"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
tags = ["aibook-fun"]
}
+44
View File
@@ -0,0 +1,44 @@
# aibook-fun - Monitoring Configuration
# Managed by OpenTofu
resource "digitalocean_monitor_alert" "cpu" {
count = var.enable_monitoring ? 1 : 0
alerts {
email = [var.alert_email]
}
window = "5m"
type = "v1/insights/droplet/cpu"
compare = "GreaterThan"
value = var.cpu_alert_threshold
enabled = true
entities = [digitalocean_droplet.web.id]
description = "aibook-fun: CPU usage above ${var.cpu_alert_threshold}% for 5 min"
}
resource "digitalocean_monitor_alert" "memory" {
count = var.enable_monitoring ? 1 : 0
alerts {
email = [var.alert_email]
}
window = "5m"
type = "v1/insights/droplet/memory_utilization_percent"
compare = "GreaterThan"
value = var.memory_alert_threshold
enabled = true
entities = [digitalocean_droplet.web.id]
description = "aibook-fun: Memory usage above ${var.memory_alert_threshold}% for 5 min"
}
resource "digitalocean_monitor_alert" "disk" {
count = var.enable_monitoring ? 1 : 0
alerts {
email = [var.alert_email]
}
window = "5m"
type = "v1/insights/droplet/disk_utilization_percent"
compare = "GreaterThan"
value = var.disk_alert_threshold
enabled = true
entities = [digitalocean_droplet.web.id]
description = "aibook-fun: Disk usage above ${var.disk_alert_threshold}% for 5 min"
}
+28
View File
@@ -0,0 +1,28 @@
# aibook-fun - Terraform Outputs
# Managed by OpenTofu
output "droplet_ip" {
description = "Droplet IPv4 address (reserved IP)"
value = digitalocean_reserved_ip.web.ip_address
}
output "reserved_ip" {
description = "Reserved IP address (alias for droplet_ip; stable across rebuilds)"
value = digitalocean_reserved_ip.web.ip_address
}
output "droplet_id" {
description = "Droplet ID"
value = digitalocean_droplet.web.id
}
output "domain" {
description = "Primary domain"
value = var.domain
}
output "site_url" {
description = "Site URL"
value = "https://${var.domain}"
}
@@ -0,0 +1,85 @@
#cloud-config
# First-boot bootstrap: base packages + Docker + git. App layer via ansible.
package_update: true
packages:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
- jq
- unzip
- git
write_files:
# Docker installation
- path: /tmp/install-docker.sh
permissions: "0755"
content: |
#!/bin/bash
set -euo pipefail
curl -fsSL https://get.docker.com -o /tmp/get-docker.sh
sh /tmp/get-docker.sh
systemctl enable docker
systemctl start docker
usermod -aG docker ubuntu
# Docker daemon config (log rotation)
- path: /etc/docker/daemon.json
content: |
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2"
}
# Encrypted-at-rest data volume setup (runs BEFORE Docker is installed).
# Graceful no-op when no volume is attached (data_volume_size_gb = 0 default
# for aibook-fun stateless marketing site).
- path: /tmp/setup-data-volume.sh
permissions: "0755"
content: |
#!/bin/bash
set -euo pipefail
DEV="/dev/disk/by-id/scsi-0DO_Volume_aibook-fun-data"
MNT="/mnt/${project_name}_data"
for i in $(seq 1 24); do
[ -e "$DEV" ] && break
sleep 5
done
if [ ! -e "$DEV" ]; then
echo "no data volume attached after 120s - using droplet local disk (stateless mode)"
exit 0
fi
blkid "$DEV" >/dev/null 2>&1 || mkfs.ext4 -q "$DEV"
mkdir -p "$MNT"
grep -q "scsi-0DO_Volume_aibook-fun-data" /etc/fstab || \
echo "$DEV $MNT ext4 defaults,noatime,nofail,discard 0 2" >> /etc/fstab
mountpoint -q "$MNT" || mount "$MNT"
mkdir -p "$MNT/docker"
jq --arg dr "$MNT/docker" '. + {"data-root": $dr}' /etc/docker/daemon.json > /tmp/daemon.json.new
mv /tmp/daemon.json.new /etc/docker/daemon.json
mkdir -p /opt/${project_name}
echo "data volume mounted at $MNT (docker data-root relocated)"
runcmd:
# Data volume setup only when a block volume is attached
- 'bash -c "[ -e /dev/disk/by-id/scsi-0DO_Volume_aibook-fun-data ] && bash /tmp/setup-data-volume.sh || echo no-data-volume"'
- [bash, /tmp/install-docker.sh]
- [systemctl, restart, docker]
# Ensure the app dir exists for ansible to land the compose + Caddyfile
- [mkdir, -p, /opt/${project_name}]
# Marker so ansible/deploy.yml can confirm the host is ready
- [touch, /opt/${project_name}/.bootstrap-complete]
# Enable automatic security updates
- dpkg-reconfigure -plow unattended-upgrades
final_message: |
aibook-fun bootstrap complete. Next step from the operator workstation:
ansible-playbook aibook-fun/ansible/deploy.yml -i 'root@<this-droplet-ip>,'
Ansible will clone the repo, build the Docker image locally on this droplet,
and bring up the compose stack. Site served at https://aibook.fun.
+27
View File
@@ -0,0 +1,27 @@
# ⚠️ NEVER commit terraform.tfvars with real values. Use ./itofu.sh instead —
# it reads TF_VAR_* straight from Infisical /tofu/.
do_token = "<YOUR_DIGITALOCEAN_TOKEN>"
ssh_key_fingerprints = ["<YOUR_SSH_KEY_FINGERPRINT>"]
# pin to admin IP/32 or VPN range for prod
ssh_source_cidrs = ["0.0.0.0/0", "::/0"]
spaces_access_key = "<YOUR_SPACES_ACCESS_KEY>"
spaces_secret_key = "<YOUR_SPACES_SECRET_KEY>"
spaces_encryption_key = "<YOUR_SSE_C_KEY_BASE64>"
domain = "aibook.fun"
region = "atl1"
droplet_size = "s-2vcpu-4gb-amd"
droplet_image = "ubuntu-24-04-x64"
data_volume_size_gb = 0
enable_monitoring = true
# MUST be verified on the DO account
alert_email = "PLT@weown.net"
cpu_alert_threshold = 80
memory_alert_threshold = 90
disk_alert_threshold = 85
+82
View File
@@ -0,0 +1,82 @@
variable "domain" {
type = string
default = "aibook.fun"
}
variable "region" {
type = string
default = "atl1"
}
variable "droplet_size" {
type = string
default = "s-2vcpu-4gb-amd"
}
variable "droplet_image" {
type = string
default = "ubuntu-24-04-x64"
}
variable "ssh_key_fingerprints" {
type = list(string)
}
variable "ssh_source_cidrs" {
description = "CIDR list allowed to reach port 22 — pin to admin IP/32 or VPN range for prod"
type = list(string)
default = ["0.0.0.0/0", "::/0"]
}
variable "do_token" {
type = string
sensitive = true
}
variable "spaces_access_key" {
type = string
sensitive = true
}
variable "spaces_secret_key" {
type = string
sensitive = true
}
variable "spaces_encryption_key" {
description = "DO Spaces SSE-C encryption key (32-byte AES-256, base64)"
type = string
sensitive = true
}
variable "data_volume_size_gb" {
description = "0 = stateless (droplet local disk); >0 attaches encrypted DO block volume for Docker data-root"
type = number
default = 0
}
variable "enable_monitoring" {
type = bool
default = true
}
variable "alert_email" {
description = "Email for monitoring alerts — MUST be verified on the DO account"
type = string
default = "PLT@weown.net"
}
variable "cpu_alert_threshold" {
type = number
default = 80
}
variable "memory_alert_threshold" {
type = number
default = 90
}
variable "disk_alert_threshold" {
type = number
default = 85
}
+17
View File
@@ -0,0 +1,17 @@
# aibook-fun - OpenTofu Version Constraints
# Managed by OpenTofu
terraform {
required_version = ">= 1.7.0"
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.36"
}
}
}
provider "digitalocean" {
token = var.do_token
}
+12 -1
View File
@@ -1,4 +1,4 @@
# AIBook.fun Marketing Site # AiBook.fun Marketing Site
Scaffolded for Framer import and Next.js (Cloudflare Pages / DigitalOcean). Scaffolded for Framer import and Next.js (Cloudflare Pages / DigitalOcean).
@@ -33,3 +33,14 @@ Note: Framer Sites don't run Next.js natively. For a live site with routing (A/B
- 3 sample tiers shown ($10 / $47yr / $4,888) - 3 sample tiers shown ($10 / $47yr / $4,888)
- No NFT/crypto/Web3 language (Kickstarter compliance scrubbed) - No NFT/crypto/Web3 language (Kickstarter compliance scrubbed)
- Event tracking only (not content) — PostHog/Cloudflare Analytics slot available - Event tracking only (not content) — PostHog/Cloudflare Analytics slot available
## Reading demo
A sample reading environment is wired into this Next.js app (pages router).
```bash
npm install
npm run dev
```
Then open [http://localhost:3000/read](http://localhost:3000/read). One chapter is filled (Real Estate, with nested RE-01 and RE-02). This is a demo, not the live book.
+2 -2
View File
@@ -13,8 +13,8 @@ export default function Authors() {
<span className="spec reveal">Who’s writing this</span> <span className="spec reveal">Who’s writing this</span>
<h2 className="reveal">Practitioners, one per vertical.</h2> <h2 className="reveal">Practitioners, one per vertical.</h2>
<p className="lede reveal"> <p className="lede reveal">
Each chapter is owned by someone who does the work — not a generalist writing about Each chapter is owned by someone who does that work for a living. Names go up as
it. Names are announced as co-authors confirm. co-authors confirm.
</p> </p>
<div className="auth-grid"> <div className="auth-grid">
{AUTHORS.map((a) => ( {AUTHORS.map((a) => (
+9 -2
View File
@@ -11,7 +11,7 @@ export default function Book3D() {
<div className="book3d__face book3d__pages" /> <div className="book3d__face book3d__pages" />
<div className="book3d__face book3d__top" /> <div className="book3d__face book3d__top" />
<div className="book3d__face book3d__spine"> <div className="book3d__face book3d__spine">
<span>AIBook.fun · Responsible Agentic AI</span> <span>AiBook.fun · Responsible Agentic AI</span>
</div> </div>
<div className="book3d__face book3d__cover"> <div className="book3d__face book3d__cover">
<span className="book3d__kicker">The Field Guide</span> <span className="book3d__kicker">The Field Guide</span>
@@ -22,12 +22,19 @@ export default function Book3D() {
</h3> </h3>
<span className="book3d__line" /> <span className="book3d__line" />
<span className="book3d__sub">Tested workflows you can actually run</span> <span className="book3d__sub">Tested workflows you can actually run</span>
<div className="book3d__foot">
<svg className="book3d__mark" viewBox="0 0 40 40" fill="none"> <svg className="book3d__mark" viewBox="0 0 40 40" fill="none">
<circle cx="20" cy="20" r="17.5" stroke="#efe9db" strokeWidth="1" opacity="0.7" /> <circle cx="20" cy="20" r="17.5" stroke="#efe9db" strokeWidth="1" opacity="0.7" />
<path d="M20 5 L22.6 20 L20 35 L17.4 20 Z" fill="#efe9db" /> <path d="M20 5 L22.6 20 L20 35 L17.4 20 Z" fill="#efe9db" />
<path d="M5 20 L20 17.4 L35 20 L20 22.6 Z" fill="#efe9db" opacity="0.55" /> <path d="M5 20 L20 17.4 L35 20 L20 22.6 Z" fill="#efe9db" opacity="0.55" />
</svg> </svg>
<span className="book3d__brand">AIBook.fun</span> <span className="book3d__brand">AiBook.fun</span>
</div>
{/* Belly band — the classic publisher's wraparound strip for a themed
tie-in, rather than redesigning the whole cover around it. */}
<div className="book3d__band">
<img src="/american-dream-banner.webp" alt="" />
</div>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -12,7 +12,7 @@ export default function Contents() {
<section className="section contents" id="contents"> <section className="section contents" id="contents">
<div className="wrap"> <div className="wrap">
<span className="spec reveal">The founding library</span> <span className="spec reveal">The founding library</span>
<h2 className="reveal">Six practices. Written by people who do the work.</h2> <h2 className="reveal">Six practices, written by people who built their careers on them.</h2>
<div className="toc reveal"> <div className="toc reveal">
{ITEMS.map((x) => ( {ITEMS.map((x) => (
<a href="#get" key={x.num}> <a href="#get" key={x.num}>
+10 -6
View File
@@ -1,24 +1,28 @@
const QA = [ const QA = [
{ {
q: "Is this AI theory, or something I can actually run?", q: "Is this AI theory, or something I can actually run?",
a: "Something you run. Every workflow shows the real tool output, lists the real steps, and ends in a ~15-minute Try-it on non-confidential sample data. If you can’t run it, it doesn’t ship.", a: "Something you run. Every workflow shows the real tool output, lists the real steps, and ends in a ~15-minute Try-it on non-confidential sample data. If you can't run it, it doesn't ship.",
open: true, open: true,
}, },
{ {
q: "How is this different from a course?", q: "How is this different from a course?",
a: "A course teaches concepts; this is a field guide of tested recipes you apply to your real job this week. It refreshes annually because AI moves fast — stale advice is worthless.", a: "A course teaches concepts. This is a field guide of tested recipes you apply to your job this week. The recipes get rewritten as the tools change, not left sitting until next year.",
}, },
{ {
q: "What does “responsible by design” actually mean?", q: "What does \"responsible by design\" actually mean?",
a: "Every recipe includes data-handling rules, human-oversight steps, failure modes, and escalation paths. They’re part of the template — not an appendix, not an afterthought.", a: "Every recipe includes data-handling rules, human-oversight steps, failure modes, and escalation paths. They sit in the template above Method, not in a footnote.",
}, },
{ {
q: "Can I contribute a workflow after backing?", q: "Can I contribute a workflow after backing?",
a: "Yes — open contribution opens once the founding library is live. We’re finalizing how contributors get paid (not just a byline) before launch.", a: "Yes. Open contribution starts once the founding library is live. How contributors get paid is still being finalized before launch.",
}, },
{ {
q: "Why subscribe instead of buy once?", q: "Why subscribe instead of buy once?",
a: "Because a static AI book is out of date in months. The subscription is what keeps every workflow current — and it’s what funds the practitioners keeping them that way.", a: "A one-time AI book goes stale in weeks. The $47/yr subscription funds ongoing updates as models and products move, often weekly or monthly.",
},
{
q: "Why frame this around the American Dream?",
a: "For most of the people writing and reading this guide, fluency with these tools is a practical ladder. Nobody in this book inherited their edge. They built it, one real skill at a time. We think this is the next one worth building.",
}, },
]; ];
+6 -6
View File
@@ -1,18 +1,18 @@
const NOTES = [ const NOTES = [
{ {
n: "01 / Stays current", n: "01 / Stays current",
h: "A subscription, not a static book", h: "Rewritten as the tools move",
p: "AI moves too fast for a book you buy once. Each annual edition is refreshed so the workflows still work when you open them.", p: "Models and products change by the week. We update recipes when a step breaks or a better path shows up, often weekly or monthly. The $47/yr subscription is what pays for that work.",
}, },
{ {
n: "02 / Authored, then open", n: "02 / Authored, then open",
h: "Written by practitioners first", h: "Co-authors fill it before launch",
p: "The first edition is seeded by co-authors — a real founding library, not an empty shelf. Open, paid contribution opens once it’s worth browsing.", p: "Founding writers are on contract. They write and test the first library. Outside contribution opens later, once there is a real catalog to join, not an empty page waiting for strangers.",
}, },
{ {
n: "03 / Responsible by design", n: "03 / Responsible by design",
h: "Guardrails in every recipe", h: "Safeguards sit above the steps",
p: "Data handling, human oversight, failure modes, escalation — part of the template on every workflow, never an appendix.", p: "Every recipe names what never goes in the prompt, who reviews before anything leaves the building, and when you stop. That block is read before Method.",
}, },
]; ];
+3 -4
View File
@@ -5,17 +5,16 @@ export default function Footer() {
<div className="foot-top"> <div className="foot-top">
<div> <div>
<div className="logo"> <div className="logo">
AIBook<b>.</b> AiBook<b>.</b>
<span className="dot">fun</span> <span className="dot">fun</span>
</div> </div>
<p className="foot-blurb"> <p className="foot-blurb">
A living, crowd-written field guide to using AI agents responsibly at work. Part An authored field guide of runnable AI workflows for work. Part of the WeOwnNet
of the WeOwnNet ecosystem. ecosystem.
</p> </p>
</div> </div>
<div className="foot-links"> <div className="foot-links">
<a href="#recipe">A recipe</a> <a href="#recipe">A recipe</a>
<a href="#contents">Contents</a>
<a href="#get">Preview</a> <a href="#get">Preview</a>
<a href="#faq">FAQ</a> <a href="#faq">FAQ</a>
</div> </div>
+4 -3
View File
@@ -27,9 +27,10 @@ export default function Hero() {
<span className="serif-it">responsibly</span> at work. <span className="serif-it">responsibly</span> at work.
</h1> </h1>
<p className="hero-sub"> <p className="hero-sub">
Not theory. Not hype. A subscription guide of{" "} A living subscription guide of{" "}
<span className="hl">tested workflows you can actually run</span> — each with the <span className="hl">tested workflows you can run</span>. Each one shows the real
real tool output, the guardrails, and a 15-minute exercise so you learn by doing. tool output, the safeguards, and a 15-minute Try-it. Recipes get updated as the
tools change.
</p> </p>
<EmailForm className="hero-form" cta="Send me the preview chapter" /> <EmailForm className="hero-form" cta="Send me the preview chapter" />
<p className="hero-note"> <p className="hero-note">
+7 -6
View File
@@ -4,16 +4,17 @@ export default function Letter() {
<div className="letter-in"> <div className="letter-in">
<span className="spec reveal">A note on why</span> <span className="spec reveal">A note on why</span>
<p className="letter-body reveal"> <p className="letter-body reveal">
Most AI advice is either hype or theory — and neither survives contact with real Most AI advice is hype or theory, and neither survives contact with real work. We are
work. We’re writing the guide we wish we’d had: workflows that <em>actually run</em>, writing the guide we wish we had: workflows that <em>actually run</em>, with the{" "}
with the <span className="mark">guardrails built in</span>, kept honest by the people <span className="mark">safeguards built in</span>, kept current by the people who use
who use them every day. Fund the first edition, and you don’t just get the book — you them every day as the tools move. For a lot of us, fluency with these tools is how you
help decide what goes in it. build the life you are working toward. Fund the first edition and you help decide what
goes in it.
</p> </p>
<div className="sign reveal"> <div className="sign reveal">
<span className="scrawl">The co-authors</span> <span className="scrawl">The co-authors</span>
<span className="who"> <span className="who">
AIBook.fun founding team AiBook.fun founding team
<br /> <br />
Real estate · finance · leadership · tech Real estate · finance · leadership · tech
</span> </span>
+29 -10
View File
@@ -1,12 +1,27 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import Link from "next/link";
const LINKS = [ const LINKS = [
{ href: "#recipe", label: "A recipe" }, { href: "#recipe", label: "A recipe" },
{ href: "#contents", label: "Contents" },
{ href: "#letter", label: "Why" }, { href: "#letter", label: "Why" },
{ href: "#faq", label: "FAQ" }, { href: "#faq", label: "FAQ" },
]; ];
function NavItem({ href, label, onClick }) {
if (href.startsWith("/")) {
return (
<Link href={href} onClick={onClick}>
{label}
</Link>
);
}
return (
<a href={href} onClick={onClick}>
{label}
</a>
);
}
export default function Nav() { export default function Nav() {
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -39,22 +54,25 @@ export default function Nav() {
return ( return (
<nav ref={navRef} className={`nav${scrolled ? " scrolled" : ""}${open ? " open" : ""}`}> <nav ref={navRef} className={`nav${scrolled ? " scrolled" : ""}${open ? " open" : ""}`}>
<div className="nav-in"> <div className="nav-in">
<a className="logo" href="#top" onClick={close}> <Link className="logo" href="/" onClick={close}>
AIBook<b>.</b> AiBook<b>.</b>
<span className="dot">fun</span> <span className="dot">fun</span>
</a> </Link>
<div className="nav-links"> <div className="nav-links">
{LINKS.map((l) => ( {LINKS.map((l) => (
<a key={l.href} href={l.href}> <NavItem key={l.href} href={l.href} label={l.label} />
{l.label}
</a>
))} ))}
</div> </div>
<div className="nav-actions">
<Link href="/read" className="btn btn-ghost nav-cta" onClick={close}>
Go to Library
</Link>
<a href="#get" className="btn btn-primary nav-cta"> <a href="#get" className="btn btn-primary nav-cta">
Get the preview Get the preview
</a> </a>
</div>
<button <button
type="button" type="button"
@@ -70,10 +88,11 @@ export default function Nav() {
<div className="nav-menu" id="mobile-menu"> <div className="nav-menu" id="mobile-menu">
{LINKS.map((l) => ( {LINKS.map((l) => (
<a key={l.href} href={l.href} onClick={close}> <NavItem key={l.href} href={l.href} label={l.label} onClick={close} />
{l.label}
</a>
))} ))}
<Link href="/read" className="btn btn-ghost" onClick={close}>
Go to Library
</Link>
<a href="#get" className="btn btn-primary" onClick={close}> <a href="#get" className="btn btn-primary" onClick={close}>
Get the preview Get the preview
</a> </a>
+1 -1
View File
@@ -34,7 +34,7 @@ export default function Recipe() {
<div className="recipe reveal"> <div className="recipe reveal">
<div className="recipe-top"> <div className="recipe-top">
<span className="recipe-id"> <span className="recipe-id">
recipe <b>RE-04</b> · v1.2 · ~15 min recipe <b>RE-01</b> · v1.2 · ~15 min
</span> </span>
<span className="recipe-vert">Real Estate</span> <span className="recipe-vert">Real Estate</span>
</div> </div>
+4 -4
View File
@@ -9,7 +9,7 @@ export default function Tiers() {
</div> </div>
<p className="tiers-note reveal"> <p className="tiers-note reveal">
Preview of planned pledge tiers. Final pricing is confirmed at the Kickstarter Preview of planned pledge tiers. Final pricing is confirmed at the Kickstarter
launch — nothing here is a live checkout yet. launch. Nothing here is a live checkout yet.
</p> </p>
</div> </div>
<div className="tier-grid"> <div className="tier-grid">
@@ -19,7 +19,7 @@ export default function Tiers() {
<div className="t-unit">one-time</div> <div className="t-unit">one-time</div>
<div className="t-desc"> <div className="t-desc">
The preview chapter, community updates, and an early look at the founding library The preview chapter, community updates, and an early look at the founding library
as it’s written. as it is written.
</div> </div>
</div> </div>
<div className="tier feature reveal"> <div className="tier feature reveal">
@@ -30,8 +30,8 @@ export default function Tiers() {
</div> </div>
<div className="t-unit">annual subscription</div> <div className="t-unit">annual subscription</div>
<div className="t-desc"> <div className="t-desc">
The full guide — PDF + web reader — and every annual update while you’re The full guide in PDF and web reader, plus ongoing recipe updates while you are
subscribed. Recurring value, no lifetime gimmick. subscribed. Billed yearly. No lifetime SKU in v1.
</div> </div>
</div> </div>
<div className="tier reveal"> <div className="tier reveal">
@@ -0,0 +1,60 @@
import { collectChapterKit } from "../../data/sample-chapter";
export default function ChapterKit({ recipes }) {
const kit = collectChapterKit(recipes);
if (!kit.tools.length && !kit.files.length) return null;
return (
<section className="chapter-kit" id="need" aria-label="What you need">
<h2 className="chapter-kit-title">What you need</h2>
<p className="chapter-kit-lede">
{recipes.length} Try-it{recipes.length === 1 ? "" : "s"} on sample data. Nothing
from a live client file.
</p>
<dl className="kit-grid">
<div>
<dt>Tools</dt>
<dd>
<ul>
{kit.tools.map((t) => (
<li key={t}>{t}</li>
))}
</ul>
</dd>
</div>
<div>
<dt>Files</dt>
<dd>
<ul>
{kit.files.map((f) => (
<li key={f}>{f}</li>
))}
</ul>
</dd>
</div>
<div>
<dt>Model</dt>
<dd>
<ul>
{kit.models.map((m) => (
<li key={m}>{m}</li>
))}
</ul>
</dd>
</div>
<div>
<dt>Try-its</dt>
<dd>
<ul>
{kit.tryits.map((t) => (
<li key={t.id}>
{t.id} · {t.time} · {t.cost}
</li>
))}
</ul>
</dd>
</div>
</dl>
</section>
);
}
@@ -0,0 +1,99 @@
import { useEffect, useMemo, useState } from "react";
function OutlineBody({ groups, complete, count, total, active, showTitle }) {
return (
<>
<p className="outline-progress">
{count}/{total} Try-its complete
</p>
{showTitle ? <p className="outline-heading">In this chapter</p> : null}
{groups.map((g) => (
<div key={g.label} className="outline-group">
<p className="outline-group-label spec">{g.label}</p>
<ul>
{g.items.map((item) => {
const done = item.recipeId ? !!complete[item.recipeId] : false;
const isActive = active === item.id;
return (
<li key={item.id}>
<a
href={`#${item.id}`}
className={isActive ? "is-active" : undefined}
aria-current={isActive ? "location" : undefined}
>
<span>{item.label}</span>
{done ? (
<span className="outline-done" aria-label="Try-it complete">
✓
</span>
) : null}
</a>
</li>
);
})}
</ul>
</div>
))}
</>
);
}
export default function ChapterOutline({
groups,
complete = {},
count = 0,
total = 0,
variant = "desktop",
}) {
const ids = useMemo(
() => groups.flatMap((g) => g.items.map((item) => item.id)),
[groups]
);
const [active, setActive] = useState(ids[0] || "");
useEffect(() => {
const els = ids.map((id) => document.getElementById(id)).filter(Boolean);
if (!els.length || typeof IntersectionObserver === "undefined") return;
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((e) => e.isIntersecting)
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
if (visible[0]?.target?.id) setActive(visible[0].target.id);
},
{ rootMargin: "-88px 0px -55% 0px", threshold: [0, 0.1, 0.25, 0.5, 1] }
);
els.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, [ids]);
if (variant === "mobile") {
return (
<details className="chapter-outline-mobile">
<summary>In this chapter</summary>
<OutlineBody
groups={groups}
complete={complete}
count={count}
total={total}
active={active}
showTitle={false}
/>
</details>
);
}
return (
<nav className="chapter-outline" aria-label="In this chapter">
<OutlineBody
groups={groups}
complete={complete}
count={count}
total={total}
active={active}
showTitle
/>
</nav>
);
}
@@ -0,0 +1,24 @@
import Link from "next/link";
export default function ReaderCrumbs({ items }) {
return (
<nav className="crumbs" aria-label="Breadcrumb">
<ol>
{items.map((item, i) => (
<li key={item.label}>
{i > 0 ? (
<span className="crumbs-sep" aria-hidden="true">
/
</span>
) : null}
{item.current || !item.href ? (
<span aria-current="page">{item.label}</span>
) : (
<Link href={item.href}>{item.label}</Link>
)}
</li>
))}
</ol>
</nav>
);
}
@@ -0,0 +1,32 @@
import Link from "next/link";
export default function ReaderFooter() {
return (
<footer className="footer reader-footer">
<div className="foot-in">
<div className="foot-top">
<div>
<div className="logo">
AiBook<b>.</b>
<span className="dot">fun</span>
</div>
<p className="foot-blurb">
Reading demo. Not the live book. Workflows sit inside a chapter,
not on a shelf of cards.
</p>
</div>
<div className="foot-links">
<Link href="/">Home</Link>
<Link href="/read">Library</Link>
<Link href="/#get">Preview</Link>
</div>
</div>
<div className="colophon">
© 2026 WeOwnAI · MIT-licensed docs · This page is a reading-environment demo.
<br />
Activation is stored in this browser only. No chatbot. No live client data.
</div>
</div>
</footer>
);
}
@@ -0,0 +1,40 @@
import { useEffect, useState } from "react";
import Link from "next/link";
export default function ReaderNav({ here = "library" }) {
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 12);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
<nav className={`nav reader-nav${scrolled ? " scrolled" : ""}`}>
<div className="nav-in">
<Link className="logo" href="/">
AiBook<b>.</b>
<span className="dot">fun</span>
</Link>
<div className="nav-links reader-links">
{here === "library" ? (
<span className="nav-current" aria-current="page">
Library
</span>
) : (
<Link href="/read" className="nav-title">
Library
</Link>
)}
</div>
<Link href="/#get" className="btn btn-primary nav-cta">
Get the preview
</Link>
</div>
</nav>
);
}
@@ -0,0 +1,60 @@
/** Same terminal chrome as the marketing Recipe (.term / .term-bar). */
export function TermRE01() {
return (
<div className="term" role="img" aria-label="Sample terminal output from the workflow">
<div className="term-bar">
<i></i>
<i></i>
<i></i>
<span>agent — property-brief · sample dataset (non-confidential)</span>
</div>
<pre>
<span className="c-dim">{"$ "}</span>
<span className="c-user">{"brief"}</span>
{" --input "}
<span className="c-key">{"sample_listings.csv"}</span>
{" --template market-1pager\n\n"}
<span className="c-dim">{"✓ redacted 3 fields (owner_name, phone, exact_address)\n✓ 41 comps in radius · median $/sqft computed\n\n"}</span>
<span className="c-ok">{"MARKET BRIEF — Maple Heights (sample)\n"}</span>
{" · Median list: $420/sqft (▲ 4.1% QoQ)\n · Days on market: 22 (was 31)\n · Inventory: 1.8 mo — seller-leaning\n · Read: price to comp, expect <2 wks at ask\n\n"}
<span className="c-dim">{"# cost this run: $0.03 · model: claude · 2,140 tokens"}</span>
</pre>
</div>
);
}
export function TermRE02() {
return (
<div className="term" role="img" aria-label="Sample terminal output from the listing-copy workflow">
<div className="term-bar">
<i></i>
<i></i>
<i></i>
<span>agent — listing-copy · sample comps (public facts)</span>
</div>
<pre>
<span className="c-dim">{"$ "}</span>
<span className="c-user">{"listing-copy"}</span>
{" --input "}
<span className="c-key">{"sample_comps.json"}</span>
{" --voice house-voice --out draft.md\n\n"}
<span className="c-dim">{"✓ used 6 public comps (no CRM fields present)\n✓ 184 words · reading grade 8\n\n"}</span>
<span className="c-ok">{"LISTING DRAFT · 14xx Cedar (sample street, public facts)\n"}</span>
{" · Bright corner lot, 3 bed / 2 bath, maple kitchen from 2019.\n · Priced to the block: comps closed $398–$441/sqft in 60 days.\n · No owner name, phone, or showing list in the prompt.\n\n"}
<span className="c-dim">{"# cost this run: $0.02 · model: claude · 1,280 tokens"}</span>
</pre>
</div>
);
}
const TERMS = {
"re-01": TermRE01,
"re-02": TermRE02,
};
export default function TermOutput({ id }) {
const Cmp = TERMS[id];
if (!Cmp) return null;
return <Cmp />;
}
@@ -0,0 +1,262 @@
import { useState } from "react";
import Link from "next/link";
import TermOutput from "./TermOutput";
const JUMP = [
{ id: "kit", label: "Kit" },
{ id: "problem", label: "Problem" },
{ id: "actors", label: "Actors" },
{ id: "setup", label: "Setup" },
{ id: "safeguards", label: "Safeguards" },
{ id: "method", label: "Method" },
{ id: "evidence", label: "Evidence" },
{ id: "tryit", label: "Try-it" },
];
function ShieldCheck() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M8 1.5 2.5 4v3.5c0 3 2.3 5.4 5.5 7 3.2-1.6 5.5-4 5.5-7V4L8 1.5Z"
stroke="#1C5D4C"
strokeWidth="1.3"
strokeLinejoin="round"
/>
<path
d="m5.6 8 1.7 1.7L10.6 6"
stroke="#1C5D4C"
strokeWidth="1.3"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function CheckMark() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<circle cx="8" cy="8" r="7" stroke="currentColor" strokeWidth="1.4" />
<path
d="m5.2 8.2 1.8 1.8 3.8-4"
stroke="currentColor"
strokeWidth="1.4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
async function copyText(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
try {
const el = document.createElement("textarea");
el.value = text;
el.setAttribute("readonly", "");
el.style.position = "fixed";
el.style.left = "-9999px";
document.body.appendChild(el);
el.select();
const ok = document.execCommand("copy");
el.remove();
return ok;
} catch {
return false;
}
}
}
function WorkflowKit({ recipe }) {
const k = recipe.kit;
if (!k) return null;
const rows = [
["You run", k.youRun],
["Tools", (k.tools || []).join(" · ")],
["Files", (k.files || []).join(" · ")],
["Model", k.model],
["Time", recipe.time],
["Cost", recipe.cost],
["You produce", k.artifact],
].filter(([, v]) => v);
return (
<dl className="workflow-kit" id={`${recipe.anchor}-kit`} aria-label="What this workflow uses">
{rows.map(([label, value]) => (
<div key={label}>
<dt>{label}</dt>
<dd>{value}</dd>
</div>
))}
</dl>
);
}
function Field({ anchor, name, label, children }) {
return (
<div className="row" id={`${anchor}-${name}`}>
<h4 className="k">{label}</h4>
<div className="v">{children}</div>
</div>
);
}
export default function WorkflowBlock({
recipe,
complete = false,
onToggleComplete,
focused = false,
index = 1,
total = 1,
}) {
const [copied, setCopied] = useState(false);
const a = recipe.anchor;
const onShare = async () => {
const ok = await copyText(recipe.shareBlurb);
if (!ok) return;
setCopied(true);
window.setTimeout(() => setCopied(false), 1800);
};
return (
<article
id={a}
className={`recipe recipe-nested${focused ? " recipe-focus" : ""}`}
aria-labelledby={`${a}-title`}
>
<div className="recipe-top">
<span className="recipe-id">
Workflow {index} of {total} · <b>{recipe.id}</b> · v{recipe.version} · {recipe.time} · {recipe.cost}
</span>
<span className="recipe-vert">{recipe.vertical}</span>
</div>
<div className="recipe-body">
<h3 className="recipe-title" id={`${a}-title`}>
{recipe.title}
</h3>
<WorkflowKit recipe={recipe} />
<nav className="recipe-jump" aria-label="In this workflow">
{JUMP.map((j) => (
<a key={j.id} href={`#${a}-${j.id}`}>
{j.label}
</a>
))}
</nav>
<Field anchor={a} name="problem" label="Problem">
{recipe.problem} <span className="sub">{recipe.problemJob}</span>
</Field>
<Field anchor={a} name="actors" label="Actors">
<ul className="plain-list">
{recipe.actors.map((line) => (
<li key={line}>{line}</li>
))}
</ul>
</Field>
<Field anchor={a} name="setup" label="Setup">
<ul className="plain-list">
{recipe.setup.map((line) => (
<li key={line}>{line}</li>
))}
</ul>
</Field>
<Field anchor={a} name="safeguards" label="Safeguards">
{recipe.safeguards.map((line) => (
<div className="guard" key={line}>
<ShieldCheck />
<span>{line}</span>
</div>
))}
</Field>
<Field anchor={a} name="method" label="Method">
<ol className="steps">
{recipe.method.map((step, i) => (
<li key={i}>
<div className="step-copy">
{step.text}
{step.term ? <TermOutput id={step.term} /> : null}
</div>
</li>
))}
</ol>
</Field>
<Field anchor={a} name="evidence" label="Evidence">
{recipe.evidence.map((line) => (
<p key={line} className="evidence-p">
{line}
</p>
))}
</Field>
<Field anchor={a} name="tryit" label="Try-it">
<p>
About {recipe.tryit.minutes} minutes. {recipe.tryit.sample}{" "}
{recipe.tryit.produce}
</p>
<p className="sub-line">
{recipe.tryit.done}{" "}
<a className="inline-sample" href={recipe.tryit.href} download>
{recipe.tryit.hrefLabel}
</a>
</p>
<p>
<span className="chip">◷ {recipe.tryit.chip}</span>
</p>
{onToggleComplete ? (
<div className="tryit-actions">
<button
type="button"
className={`btn ${complete ? "btn-primary tryit-done" : "btn-ghost"}`}
aria-pressed={complete}
onClick={() => onToggleComplete(recipe.id)}
>
{complete ? <CheckMark /> : null}
{complete ? "Counted as activation (demo)" : "Mark complete"}
</button>
</div>
) : null}
</Field>
<Field anchor={a} name="share" label="Share">
<p>Copy a short result line. Share the outcome, not the recipe text.</p>
<div className="tryit-actions">
<button type="button" className="btn btn-ghost" onClick={onShare}>
{copied ? "Copied" : "Copy result line"}
</button>
</div>
</Field>
<Field anchor={a} name="record" label="Record">
<div className="recipe-record">
<span>Author: {recipe.author}</span>
<span>Sources: {recipe.sources}</span>
<span>Last updated: {recipe.lastUpdated}</span>
{focused ? (
<span>
<Link href={`/read/real-estate#${recipe.anchor}`}>
Back to the chapter
</Link>
</span>
) : (
<span>
<Link href={`/read/recipe/${recipe.id}`}>Open on paper</Link>
</span>
)}
</div>
</Field>
</div>
</article>
);
}
@@ -0,0 +1,35 @@
export default function WorkflowRail({ recipes, complete = {}, total }) {
return (
<aside className="chapter-rail" aria-label="Workflows in this chapter">
<p className="spec">Workflows in this chapter</p>
<ol className="rail-list">
{recipes.map((r, i) => {
const done = !!complete[r.id];
return (
<li key={r.id}>
<a className={`rail-card${done ? " is-done" : ""}`} href={`#${r.anchor}`}>
<span className="rail-kicker">
{i + 1} of {total} · {r.id}
</span>
<span className="rail-title">{r.shortTitle}</span>
<span className="rail-meta">
{r.time} · {r.cost}
{done ? " · done" : ""}
</span>
{r.kit && r.kit.tools && r.kit.tools.length ? (
<span className="rail-kit">{r.kit.tools.join(" · ")}</span>
) : null}
{r.kit && r.kit.artifact ? (
<span className="rail-kit rail-out">{r.kit.artifact}</span>
) : null}
</a>
</li>
);
})}
</ol>
<a className="rail-skip" href={`#${recipes[0]?.anchor}-tryit`}>
Skip to the first Try-it
</a>
</aside>
);
}
@@ -0,0 +1,43 @@
import { useCallback, useEffect, useMemo, useState } from "react";
function keyFor(id) {
return `aibook-demo-activation:${id}`;
}
/**
* Demo-only activation state. Completing a Try-it writes
* localStorage key `aibook-demo-activation:<RECIPE-ID>`.
*/
export default function useActivation(ids) {
const list = useMemo(() => ids, [ids.join("|")]);
const [complete, setComplete] = useState(() =>
Object.fromEntries(list.map((id) => [id, false]))
);
useEffect(() => {
const next = {};
list.forEach((id) => {
try {
next[id] = window.localStorage.getItem(keyFor(id)) === "1";
} catch {
next[id] = false;
}
});
setComplete(next);
}, [list]);
const toggle = useCallback((id) => {
setComplete((prev) => {
const value = !prev[id];
try {
window.localStorage.setItem(keyFor(id), value ? "1" : "0");
} catch {
/* private mode: still toggle in memory */
}
return { ...prev, [id]: value };
});
}, []);
const count = list.filter((id) => complete[id]).length;
return { complete, toggle, count, total: list.length };
}
+311
View File
@@ -0,0 +1,311 @@
/**
* Sample chapter + nested recipes for the reading demo.
* Imported by pages under /read. Not a CMS.
*/
export const LIBRARY = [
{
num: "01",
name: "Real Estate",
slug: "real-estate",
href: "/read/real-estate",
desc: "Market briefs and listings, without leaking client data",
status: "ready",
},
{
num: "02",
name: "Financial Advisors",
slug: "financial-advisors",
desc: "Portfolio summaries that stay compliant and reviewed",
status: "coming",
},
{
num: "03",
name: "Fractional C-Suite",
slug: "fractional-c-suite",
desc: "Board updates and plans with real escalation paths",
status: "coming",
},
{
num: "04",
name: "Higher Ed",
slug: "higher-ed",
desc: "Course and research workflows with academic integrity",
status: "coming",
},
{
num: "05",
name: "Communities",
slug: "communities",
desc: "Moderation and programming that respects members",
status: "coming",
},
{
num: "06",
name: "Technology",
slug: "technology",
desc: "Agent patterns, evals, and the responsible defaults",
status: "coming",
},
];
const RE01 = {
id: "RE-01",
anchor: "re-01",
version: "1.2",
time: "~15 min",
cost: "≈ $0.03",
vertical: "Real Estate",
shortTitle: "Market brief",
title: "Draft a property-market brief with an AI agent without leaking client data.",
problem:
"You need a neighborhood market brief for a client meeting this week. The listing sheet and client notes cannot go into a public model.",
problemJob:
"Job: turn messy local data into a credible one-pager in minutes, safely.",
actors: [
"Listing agent runs it.",
"Client receives the brief.",
"Model may draft and classify. A human must review before send.",
],
setup: [
"Tools: local redaction script (brief CLI), Claude or your usual model.",
"Sample files: sample_listings.csv, market-1pager template.",
"Cost: about $0.03 on the sample run (2,140 tokens).",
],
safeguards: [
"Redaction runs on your machine. Raw client data never reaches the model.",
"A human checklist is required before the brief is shared. The checklist is part of the recipe.",
"Stop if identifiers remain after the strip report. Do not prompt until owner_name, phone, and exact_address are gone.",
],
method: [
{
text: "Strip identifiers from the source data locally (a provided script does this).",
},
{
text: "Prompt the agent with the anonymized sample and a fixed brief template.",
term: "re-01",
},
{
text: "Review the draft against the checklist before anything leaves your machine.",
},
],
evidence: [
"A one-pager brief on Maple Heights (sample), ready for a meeting once a human signs the checklist.",
"Sample metrics from that run: median $420/sqft, 22 days on market, 1.8 months of inventory.",
],
tryit: {
minutes: 15,
sample: "Use the included sample_listings.csv (no live client file).",
produce: "Produce the market-1pager brief and walk the human checklist.",
done: "Mark complete when you have a brief you would send after review. Completing this is the activation demo.",
href: "/samples/sample_listings.csv",
hrefLabel: "sample_listings.csv",
chip: "cost to try ≈ $0.03",
},
shareBlurb:
"Ran RE-01 on sample comps. Maple Heights median $420/sqft, 22 DOM.",
author: "Founding library · sample",
sources: "Sample MLS packet (non-confidential); market-1pager template shipped with the recipe.",
lastUpdated: "31 Aug 2026",
kit: {
youRun: "Listing agent",
tools: ["brief CLI (local redaction)", "Claude"],
files: ["sample_listings.csv", "market-1pager template"],
model: "Claude",
artifact: "One-pager market brief",
},
};
const RE02 = {
id: "RE-02",
anchor: "re-02",
version: "1.0",
time: "~12 min",
cost: "≈ $0.02",
vertical: "Real Estate",
shortTitle: "Listing copy",
title: "Rewrite a listing description from public comps without pasting the client address book.",
problem:
"You need listing copy that sounds like this house, not every house on the street. The CRM holds owner notes and the showing list. Those do not go into a public model.",
problemJob:
"Job: draft 180-word listing copy from public facts only, then edit it yourself.",
actors: [
"Listing agent runs it.",
"The public listing receives the copy.",
"Model drafts. A human edits, files, and posts to MLS.",
],
setup: [
"Tools: listing-copy CLI, listing-voice template, Claude or your usual model.",
"Sample files: sample_comps.json (public comps only).",
"Cost: about $0.02 on the sample run (1,280 tokens).",
],
safeguards: [
"Prompt only with public listing facts. No CRM export, no showing list, no owner notes.",
"A human edits the draft before it hits MLS. Do not auto-post.",
"Stop if a private note, phone, or exact owner name appears in the draft.",
],
method: [
{
text: "Export public comps for the block. Leave the CRM closed.",
},
{
text: "Run listing-copy against the sample comps and the house-voice template.",
term: "re-02",
},
{
text: "Edit the draft. Cut anything that could only have come from a private note.",
},
],
evidence: [
"A 184-word listing draft, public facts only, reading grade 8.",
"Comps on the block closed between $398 and $441/sqft in 60 days. The draft prices to that range without naming a client.",
],
tryit: {
minutes: 12,
sample: "Use the included sample_comps.json (public comps, no address book).",
produce: "Produce a ~180-word listing description and a human edit pass.",
done: "Mark complete when the draft has no private fields. Completing this is the activation demo.",
href: "/samples/sample_comps.json",
hrefLabel: "sample_comps.json",
chip: "cost to try ≈ $0.02",
},
shareBlurb:
"Ran RE-02 on sample comps. 184-word listing draft, public facts only.",
author: "Founding library · sample",
sources: "Public comps sample packet; listing-voice template shipped with the recipe.",
lastUpdated: "31 Aug 2026",
kit: {
youRun: "Listing agent",
tools: ["listing-copy CLI", "Claude"],
files: ["sample_comps.json", "listing-voice template"],
model: "Claude",
artifact: "180-word listing draft",
},
};
export const RECIPES = [RE01, RE02];
export const RECIPES_BY_ID = {
"RE-01": RE01,
"RE-02": RE02,
// Old demo URLs. Displayed ID is RE-01 / RE-02.
"RE-04": RE01,
"RE-05": RE02,
};
export const CHAPTER = {
slug: "real-estate",
vertical: "Real Estate",
edition: "2026",
authors: "Founding library · sample",
title: "Work the listing without leaking the client",
promise:
"In this chapter you run two jobs a realtor actually has this week, on sample data, without putting a client file in a public model.",
lastUpdated: "31 Aug 2026",
skip: [
{ id: "RE-01", href: "#re-01", title: "Market brief" },
{ id: "RE-02", href: "#re-02", title: "Listing copy" },
],
stakes: {
id: "stakes",
heading: "The stakes",
paragraphs: [
"A listing that stalls because the market brief is late is a lost weekend. A listing that leaks because you pasted the client sheet into a public model is a lost license. Real estate work runs on both clocks: the showing at 4pm, and the file that must never leave the office.",
"The failure is not that AI sits unused. It is a one-pager that is late, generic, or built from a client name that should not have been in the prompt. Buyers notice fluff. Brokers notice a privacy incident. Neither is recoverable in the same week.",
],
},
constraints: {
id: "constraints",
heading: "Constraints you cannot ignore",
paragraphs: [
"You already work in MLS exports, a CRM, and a phone that rings during the write-up. Done is a brief the client can read in a meeting, with numbers you can stand behind, reviewed by a human. Public models are useful for draft and classify. They are not a filing cabinet.",
"Owner names, phones, exact addresses, and showing lists stay on your machine. Comps, medians, and days on market can travel, once identifiers are stripped. If a field could pick one household out of the block, it does not go in the prompt.",
],
},
practice: {
id: "practice",
heading: "How we work here",
paragraphs: [
"Redact locally. Prompt with a sample or a stripped file. Review against a checklist. Share the artifact, not the workflow text. The first job this week is the neighborhood market brief. Run it on sample comps before you touch a live listing.",
],
},
between: {
id: "between",
heading: "The brief is half the meeting",
paragraphs: [
"The brief gets you into the room with numbers. The other half is the listing copy: public comps, a description that does not sound like every other house on the street, and an address book that never belongs in the prompt. Same rule. Different job.",
],
},
further: [
{ id: "RE-01", href: "#re-01", title: "Draft a property-market brief without leaking client data" },
{ id: "RE-02", href: "#re-02", title: "Rewrite a listing description from public comps" },
{ id: "RE-03", title: "Offer-response notes from public remarks (later)", coming: true },
],
sources: [
"Sample MLS packet shipped with this chapter (non-confidential).",
"market-1pager and listing-voice templates in the recipe kit.",
"Local redaction fields: owner_name, phone, exact_address.",
],
};
/** Outline groups for the chapter map. Headings come from the essay objects. */
export function buildOutline(chapter, recipes) {
return [
{
label: "Start",
items: [{ id: "need", label: "What you need" }],
},
{
label: "Essay",
items: [chapter.stakes, chapter.constraints, chapter.practice, chapter.between].map((s) => ({
id: s.id,
label: s.heading,
})),
},
{
label: "Workflows",
items: recipes.map((r) => ({
id: r.anchor,
recipeId: r.id,
label: r.id + " · " + r.shortTitle,
time: r.time,
shortTitle: r.shortTitle,
tools: r.kit && r.kit.tools ? r.kit.tools.join(", ") : "",
})),
},
{
label: "End",
items: [
{ id: "further", label: "Further workflows" },
{ id: "sources", label: "Sources" },
],
},
];
}
/** Union of recipe kits for the chapter "What you need" strip. */
export function collectChapterKit(recipes) {
const tools = [];
const files = [];
const models = [];
const add = (arr, value) => {
if (value && !arr.includes(value)) arr.push(value);
};
recipes.forEach((r) => {
const k = r.kit || {};
(k.tools || []).forEach((item) => add(tools, item));
(k.files || []).forEach((item) => add(files, item));
add(models, k.model);
});
return {
tools,
files,
models,
tryits: recipes.map((r) => ({
id: r.id,
time: r.time,
cost: r.cost,
artifact: r.kit && r.kit.artifact,
})),
};
}
+4
View File
@@ -1,6 +1,10 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
// ⚠️ REQUIRED by deploy/docker/Dockerfile — do not remove.
// Emits .next/standalone/ which the multi-stage Docker build copies into
// the runner image (Dockerfile line 34). Removing this breaks CI/CD.
output: 'standalone',
// A stray package-lock.json sits at the repo root; pin the workspace root here // A stray package-lock.json sits at the repo root; pin the workspace root here
// so Turbopack doesn't infer the wrong one. // so Turbopack doesn't infer the wrong one.
turbopack: { turbopack: {
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aibook-fun-marketing", "name": "aibook-fun-marketing",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"description": "AIBook.fun — pre-launch marketing site (field-guide design)", "description": "AiBook.fun — pre-launch marketing site (field-guide design)",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "next dev -p 3000", "dev": "next dev -p 3000",
+2 -2
View File
@@ -18,10 +18,10 @@ export default function Home() {
return ( return (
<> <>
<Head> <Head>
<title>AIBook.fun — a field guide to AI agents at work</title> <title>AiBook.fun · a field guide to AI agents at work</title>
<meta <meta
name="description" name="description"
content="A living, crowd-written field guide to using AI agents responsibly at work. Tested workflows you can run — launching on Kickstarter, Sept 26, 2026." content="An authored field guide of runnable AI workflows for work. Updated as the tools change. Kickstarter Sept 26, 2026. $47/yr."
/> />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
</Head> </Head>
+56
View File
@@ -0,0 +1,56 @@
import Head from "next/head";
import Link from "next/link";
import ReaderNav from "../../components/reader/ReaderNav";
import ReaderFooter from "../../components/reader/ReaderFooter";
import { LIBRARY, RECIPES } from "../../data/sample-chapter";
export default function ReadLibrary() {
return (
<>
<Head>
<title>Library · AiBook.fun reading demo</title>
<meta
name="description"
content="Library for the 2026 AiBook.fun field guide. Six verticals. One preview chapter open."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<ReaderNav />
<main>
<section className="section contents reader-library">
<div className="wrap reader-wide">
<span className="spec demo-chip">Reading demo · not the live book</span>
<h1 className="library-title">Library</h1>
<p className="library-lede">
Six verticals for the 2026 edition.
</p>
<div className="toc">
{LIBRARY.map((item) =>
item.status === "ready" ? (
<Link href={item.href} key={item.num}>
<span className="num">{item.num}</span>
<span className="name">{item.name}</span>
<span className="desc">{item.desc}</span>
<span className="toc-count">{RECIPES.length} workflows</span>
<span className="arrow" aria-hidden="true">
→
</span>
</Link>
) : (
<div className="toc-soon" key={item.num} aria-disabled="true">
<span className="num">{item.num}</span>
<span className="name">{item.name}</span>
<span className="desc">{item.desc}</span>
<span className="soon">Coming</span>
</div>
)
)}
</div>
</div>
</section>
</main>
<ReaderFooter />
</>
);
}
+208
View File
@@ -0,0 +1,208 @@
import { useEffect, useMemo } from "react";
import Head from "next/head";
import ReaderNav from "../../components/reader/ReaderNav";
import ReaderCrumbs from "../../components/reader/ReaderCrumbs";
import ReaderFooter from "../../components/reader/ReaderFooter";
import WorkflowBlock from "../../components/reader/WorkflowBlock";
import ChapterOutline from "../../components/reader/ChapterOutline";
import WorkflowRail from "../../components/reader/WorkflowRail";
import ChapterKit from "../../components/reader/ChapterKit";
import useActivation from "../../components/reader/useActivation";
import { CHAPTER, RECIPES, buildOutline } from "../../data/sample-chapter";
const RECIPE_IDS = RECIPES.map((r) => r.id);
const ESSAY_KEYS = ["stakes", "constraints", "practice"];
function Essay({ id, heading, paragraphs }) {
return (
<section className="essay" id={id}>
<h2>{heading}</h2>
{paragraphs.map((p) => (
<p key={p}>{p}</p>
))}
</section>
);
}
export default function RealEstateChapter() {
const { complete, toggle, count, total } = useActivation(RECIPE_IDS);
const [first, second] = RECIPES;
const outline = useMemo(() => buildOutline(CHAPTER, RECIPES), []);
const essayGroup = outline.find((g) => g.label === "Essay");
const workflowGroup = outline.find((g) => g.label === "Workflows");
useEffect(() => {
const hash = window.location.hash;
if (!hash) return;
const el = document.getElementById(hash.slice(1));
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
}, []);
return (
<>
<Head>
<title>{CHAPTER.title} · AiBook.fun reading demo</title>
<meta name="description" content={CHAPTER.promise} />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<ReaderNav here="chapter" />
<main>
<article className="chapter-page">
<div className="wrap reader-wide">
<ReaderCrumbs
items={[
{ href: "/read", label: "Library" },
{ label: CHAPTER.vertical, current: true },
]}
/>
</div>
<div className="wrap reader-wide chapter-shell">
<ChapterOutline
groups={outline}
complete={complete}
count={count}
total={total}
/>
<div className="chapter-col">
<span className="spec demo-chip">Reading demo · not the live book</span>
<p className="spec chapter-kicker">
{CHAPTER.vertical} · {CHAPTER.edition} edition
</p>
<h1 className="chapter-title">{CHAPTER.title}</h1>
<p className="chapter-by">{CHAPTER.authors}</p>
<ChapterOutline
variant="mobile"
groups={outline}
complete={complete}
count={count}
total={total}
/>
<p className="chapter-promise">{CHAPTER.promise}</p>
<ChapterKit recipes={RECIPES} />
<section className="chapter-map" aria-label="What's in this chapter">
<h2 className="chapter-map-title">What's in this chapter</h2>
<div className="chapter-map-block">
<p className="spec">Essay</p>
<ul>
{essayGroup.items.map((item) => (
<li key={item.id}>
<a href={`#${item.id}`}>{item.label}</a>
</li>
))}
</ul>
</div>
<div className="chapter-map-block">
<p className="spec">Workflows</p>
<ul>
{workflowGroup.items.map((item) => (
<li key={item.id}>
<a href={`#${item.id}`}>
{item.recipeId} · {item.shortTitle} · {item.time}
{item.tools ? ` · ${item.tools}` : ""}
</a>
</li>
))}
</ul>
</div>
</section>
<div className="skip-row">
<span className="skip-label">Skip to a Try-it</span>
{RECIPES.map((r) => (
<a key={r.id} className="skip-chip" href={`#${r.anchor}`}>
{r.id}
<span className="skip-chip-job">{r.shortTitle}</span>
</a>
))}
</div>
{ESSAY_KEYS.map((key) => (
<Essay
key={key}
id={CHAPTER[key].id}
heading={CHAPTER[key].heading}
paragraphs={CHAPTER[key].paragraphs}
/>
))}
<p className="spec workflow-kicker">
Workflow 1 of {RECIPES.length} in this chapter
</p>
<WorkflowBlock
recipe={first}
index={1}
total={RECIPES.length}
complete={!!complete[first.id]}
onToggleComplete={toggle}
/>
<Essay
id={CHAPTER.between.id}
heading={CHAPTER.between.heading}
paragraphs={CHAPTER.between.paragraphs}
/>
<p className="spec workflow-kicker">
Workflow 2 of {RECIPES.length} in this chapter
</p>
<WorkflowBlock
recipe={second}
index={2}
total={RECIPES.length}
complete={!!complete[second.id]}
onToggleComplete={toggle}
/>
<section className="end-matter" id="further">
<h2>Further workflows in this vertical</h2>
<ul className="further-list">
{CHAPTER.further.map((item) => (
<li key={item.id}>
{item.coming ? (
<>
<span className="recipe-id">
<b>{item.id}</b>
</span>{" "}
{item.title}
</>
) : (
<>
<a href={item.href}>
<span className="recipe-id">
<b>{item.id}</b>
</span>
</a>{" "}
{item.title}
</>
)}
</li>
))}
</ul>
</section>
<section className="end-sources" id="sources">
<h2>Sources</h2>
<ul className="further-list">
{CHAPTER.sources.map((s) => (
<li key={s}>{s}</li>
))}
</ul>
<p className="chapter-updated">Last updated: {CHAPTER.lastUpdated}</p>
</section>
</div>
<WorkflowRail recipes={RECIPES} complete={complete} total={total} />
</div>
</article>
</main>
<ReaderFooter />
</>
);
}
+91
View File
@@ -0,0 +1,91 @@
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import ReaderNav from "../../../components/reader/ReaderNav";
import ReaderCrumbs from "../../../components/reader/ReaderCrumbs";
import ReaderFooter from "../../../components/reader/ReaderFooter";
import WorkflowBlock from "../../../components/reader/WorkflowBlock";
import useActivation from "../../../components/reader/useActivation";
import { RECIPES, RECIPES_BY_ID } from "../../../data/sample-chapter";
export default function RecipeFocus() {
const router = useRouter();
const raw = typeof router.query.id === "string" ? router.query.id : "";
const recipe = RECIPES_BY_ID[raw] || RECIPES_BY_ID[raw.toUpperCase()] || null;
const { complete, toggle } = useActivation(recipe ? [recipe.id] : []);
if (!router.isReady) {
return (
<>
<ReaderNav here="chapter" />
<main>
<div className="wrap reader-wide recipe-focus-page">
<p className="spec">Loading recipe…</p>
</div>
</main>
</>
);
}
if (!recipe) {
return (
<>
<Head>
<title>Recipe not in this demo · AiBook.fun</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<ReaderNav />
<main>
<div className="wrap reader-wide recipe-focus-page">
<span className="spec demo-chip">Reading demo · not the live book</span>
<h1 className="chapter-title">This recipe is not in the sample.</h1>
<p className="library-lede">
The demo ships two nested workflows, RE-01 and RE-02, inside the Real
Estate chapter.
</p>
<p>
<Link href="/read/real-estate" className="btn btn-primary">
Back to the chapter
</Link>
</p>
</div>
</main>
<ReaderFooter />
</>
);
}
return (
<>
<Head>
<title>{`${recipe.id} on paper · AiBook.fun reading demo`}</title>
<meta name="description" content={recipe.title} />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<ReaderNav here="chapter" />
<main>
<div className="wrap reader-wide recipe-focus-page">
<ReaderCrumbs
items={[
{ href: "/read", label: "Library" },
{ href: "/read/real-estate", label: "Real Estate" },
{ label: recipe.id, current: true },
]}
/>
<span className="spec demo-chip">Reading demo · not the live book</span>
<p className="spec chapter-kicker">Print / focus view</p>
<WorkflowBlock
recipe={recipe}
index={Math.max(RECIPES.findIndex((r) => r.id === recipe.id) + 1, 1)}
total={RECIPES.length}
complete={!!complete[recipe.id]}
onToggleComplete={toggle}
focused
/>
</div>
</main>
<ReaderFooter />
</>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

@@ -0,0 +1,19 @@
{
"note": "Public comps only. No CRM fields, no owner names, no phones, no showing lists.",
"block": "14xx Cedar (sample street)",
"subject": {
"beds": 3,
"baths": 2,
"sqft": 1320,
"year_kitchen": 2019,
"lot": "corner, public record"
},
"comps": [
{ "id": "C1", "closed_price_sqft": 398, "dom": 19, "beds": 3, "baths": 2, "closed_days_ago": 18 },
{ "id": "C2", "closed_price_sqft": 441, "dom": 11, "beds": 3, "baths": 2, "closed_days_ago": 27 },
{ "id": "C3", "closed_price_sqft": 412, "dom": 22, "beds": 3, "baths": 2, "closed_days_ago": 41 },
{ "id": "C4", "closed_price_sqft": 425, "dom": 16, "beds": 3, "baths": 3, "closed_days_ago": 9 },
{ "id": "C5", "closed_price_sqft": 405, "dom": 28, "beds": 2, "baths": 2, "closed_days_ago": 55 },
{ "id": "C6", "closed_price_sqft": 433, "dom": 14, "beds": 3, "baths": 2, "closed_days_ago": 33 }
]
}
@@ -0,0 +1,7 @@
id,neighborhood,list_price,sqft,dom,owner_name,phone,exact_address,beds,baths
MH-01,Maple Heights,504000,1200,18,Jane Client,555-0140,12 Oak Street,3,2
MH-02,Maple Heights,612000,1450,24,Sam Owner,555-0141,88 Birch Avenue,3,2
MH-03,Maple Heights,459000,1100,31,R. Patel,555-0142,4 Cedar Court,2,2
MH-04,Maple Heights,670000,1580,12,Lee Family Trust,555-0143,21 Maple Drive,4,3
MH-05,Maple Heights,538000,1288,22,A. Gomez,555-0144,9 Pine Lane,3,2
MH-06,Maple Heights,481000,1140,27,Hidden,555-0145,16 Walnut Place,3,1
1 id neighborhood list_price sqft dom owner_name phone exact_address beds baths
2 MH-01 Maple Heights 504000 1200 18 Jane Client 555-0140 12 Oak Street 3 2
3 MH-02 Maple Heights 612000 1450 24 Sam Owner 555-0141 88 Birch Avenue 3 2
4 MH-03 Maple Heights 459000 1100 31 R. Patel 555-0142 4 Cedar Court 2 2
5 MH-04 Maple Heights 670000 1580 12 Lee Family Trust 555-0143 21 Maple Drive 4 3
6 MH-05 Maple Heights 538000 1288 22 A. Gomez 555-0144 9 Pine Lane 3 2
7 MH-06 Maple Heights 481000 1140 27 Hidden 555-0145 16 Walnut Place 3 1
+486 -17
View File
@@ -1,7 +1,7 @@
@import "tailwindcss"; @import "tailwindcss";
/* ============================================================ /* ============================================================
AIBook.fun — "field guide" design system AiBook.fun — "field guide" design system
Tokens live in @theme so they're available BOTH as CSS vars Tokens live in @theme so they're available BOTH as CSS vars
(var(--color-pine)) and as Tailwind utilities (bg-pine, font-display). (var(--color-pine)) and as Tailwind utilities (bg-pine, font-display).
============================================================ */ ============================================================ */
@@ -104,21 +104,33 @@ h1, h2, h3 { text-wrap: balance; margin: 0; }
/* ============ NAV ============ */ /* ============ NAV ============ */
.nav { .nav {
position: sticky; top: 0; z-index: 50; position: sticky; top: 0; z-index: 50;
background: color-mix(in srgb, var(--color-paper) 82%, transparent); background: var(--color-paper);
backdrop-filter: blur(14px) saturate(1.3); -webkit-backdrop-filter: blur(14px) saturate(1.3);
border-bottom: 1px solid transparent; transition: border-color 0.3s, box-shadow 0.3s; border-bottom: 1px solid transparent; transition: border-color 0.3s, box-shadow 0.3s;
} }
.nav.scrolled { border-bottom-color: var(--color-line); box-shadow: var(--sh-sm); } .nav.scrolled { border-bottom-color: var(--color-line); box-shadow: var(--sh-sm); }
.nav-in { .nav-in {
position: relative;
max-width: var(--wrap); margin: 0 auto; padding: 0 28px; height: 68px; max-width: var(--wrap); margin: 0 auto; padding: 0 28px; height: 68px;
display: flex; align-items: center; justify-content: space-between; gap: 20px; display: flex; align-items: center; justify-content: space-between; gap: 20px;
} }
.logo { display: flex; align-items: baseline; gap: 2px; font-family: var(--font-display); font-weight: 900; font-size: 21px; letter-spacing: -0.02em; } .logo {
display: flex; align-items: baseline; gap: 2px;
font-family: var(--font-display); font-weight: 900; font-size: 21px; letter-spacing: -0.02em;
flex: none; position: relative; z-index: 1;
}
.logo b { color: var(--color-pine); } .logo b { color: var(--color-pine); }
.logo .dot { color: var(--color-muted); font-weight: 500; } .logo .dot { color: var(--color-muted); font-weight: 500; }
.nav-links { display: flex; align-items: center; gap: 30px; font-size: 14.5px; font-weight: 600; color: var(--color-muted); } .nav-links {
display: flex; align-items: center; gap: 30px;
font-size: 14.5px; font-weight: 600; color: var(--color-muted);
position: absolute; left: 50%; transform: translateX(-50%);
}
.nav-links a { transition: color 0.18s; } .nav-links a { transition: color 0.18s; }
.nav-links a:hover { color: var(--color-ink); } .nav-links a:hover { color: var(--color-ink); }
.nav-actions {
display: flex; align-items: center; gap: 10px;
flex: none; margin-left: auto; position: relative; z-index: 1;
}
.nav-cta { font-size: 14px; padding: 10px 18px; } .nav-cta { font-size: 14px; padding: 10px 18px; }
/* mobile hamburger toggle (hidden on desktop) */ /* mobile hamburger toggle (hidden on desktop) */
@@ -144,6 +156,7 @@ h1, h2, h3 { text-wrap: balance; margin: 0; }
@media (max-width: 860px) { @media (max-width: 860px) {
.nav-links { display: none; } .nav-links { display: none; }
.nav-actions { display: none; }
.nav-cta { display: none; } /* CTA moves into the menu */ .nav-cta { display: none; } /* CTA moves into the menu */
.nav-toggle { display: inline-flex; } .nav-toggle { display: inline-flex; }
.nav-menu { .nav-menu {
@@ -191,7 +204,7 @@ h1, h2, h3 { text-wrap: balance; margin: 0; }
.hero-visual { display: flex; justify-content: center; order: 2; } /* book on the right */ .hero-visual { display: flex; justify-content: center; order: 2; } /* book on the right */
.hero-copy { min-width: 0; order: 1; } /* text on the left */ .hero-copy { min-width: 0; order: 1; } /* text on the left */
.book3d { --bw: 300px; --bh: 420px; --bt: 54px; position: relative; perspective: 2000px; flex: none; padding: 6px 0 40px; } .book3d { --bw: 300px; --bh: 420px; --bt: 54px; --band-h: 58px; position: relative; perspective: 2000px; flex: none; padding: 6px 0 40px; }
.book3d__inner { .book3d__inner {
position: relative; width: var(--bw); height: var(--bh); margin: 0 auto; position: relative; width: var(--bw); height: var(--bh); margin: 0 auto;
transform-style: preserve-3d; transform: rotateX(7deg) rotateY(-34deg); transform-style: preserve-3d; transform: rotateX(7deg) rotateY(-34deg);
@@ -208,7 +221,7 @@ h1, h2, h3 { text-wrap: balance; margin: 0; }
transform: translateZ(calc(var(--bt) / 2)); border-radius: 3px 8px 8px 3px; overflow: hidden; transform: translateZ(calc(var(--bt) / 2)); border-radius: 3px 8px 8px 3px; overflow: hidden;
background: linear-gradient(125deg, #23745f, #1a5748 48%, #123f34); background: linear-gradient(125deg, #23745f, #1a5748 48%, #123f34);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.07), inset 8px 0 16px -8px rgba(0, 0, 0, 0.5), 0 32px 46px -22px rgba(18, 63, 52, 0.55); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.07), inset 8px 0 16px -8px rgba(0, 0, 0, 0.5), 0 32px 46px -22px rgba(18, 63, 52, 0.55);
padding: 32px 27px 27px; display: flex; flex-direction: column; padding: 32px 27px calc(var(--band-h) * 1.48); display: flex; flex-direction: column;
} }
.book3d__back { transform: rotateY(180deg) translateZ(calc(var(--bt) / 2)); border-radius: 8px 3px 3px 8px; background: #0f3b30; } .book3d__back { transform: rotateY(180deg) translateZ(calc(var(--bt) / 2)); border-radius: 8px 3px 3px 8px; background: #0f3b30; }
.book3d__pages { .book3d__pages {
@@ -235,8 +248,21 @@ h1, h2, h3 { text-wrap: balance; margin: 0; }
.book3d__title { font-family: var(--font-display); font-weight: 600; font-size: 2.25rem; line-height: 1.02; letter-spacing: -0.02em; color: #f3eee2; margin-top: 16px; } .book3d__title { font-family: var(--font-display); font-weight: 600; font-size: 2.25rem; line-height: 1.02; letter-spacing: -0.02em; color: #f3eee2; margin-top: 16px; }
.book3d__line { width: 34px; height: 2px; background: rgba(239, 233, 219, 0.5); margin: 15px 0; } .book3d__line { width: 34px; height: 2px; background: rgba(239, 233, 219, 0.5); margin: 15px 0; }
.book3d__sub { font-family: var(--font-body); font-size: 13px; color: rgba(239, 233, 219, 0.68); line-height: 1.45; max-width: 16ch; } .book3d__sub { font-family: var(--font-body); font-size: 13px; color: rgba(239, 233, 219, 0.68); line-height: 1.45; max-width: 16ch; }
.book3d__mark { width: 44px; height: 44px; margin-top: auto; } .book3d__foot { display: flex; align-items: center; gap: 10px; margin-top: auto; }
.book3d__brand { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.1em; color: rgba(239, 233, 219, 0.85); margin-top: 12px; } .book3d__mark { width: 30px; height: 30px; flex: none; }
/* Belly band: a promotional strip wrapped across the lower cover, publisher-
style, rather than redesigning the whole face around the source graphic.
The <img> is width-locked to the band and shifted up so the crop stays
full-width (legible "AMERICAN" wordmark) and only trims top/bottom. */
.book3d__band {
position: absolute; left: 0; right: 0; bottom: calc(var(--band-h) * 0.31);
height: var(--band-h); overflow: hidden;
box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.1) inset, 0 3px 10px rgba(0, 0, 0, 0.3);
}
.book3d__band img {
position: absolute; left: 0; top: -78%; width: 100%; height: auto; display: block;
}
.book3d__brand { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.1em; color: rgba(239, 233, 219, 0.85); }
.book3d::after { .book3d::after {
content: ""; position: absolute; left: 50%; bottom: 6px; width: 60%; height: 24px; transform: translateX(-50%); content: ""; position: absolute; left: 50%; bottom: 6px; width: 60%; height: 24px; transform: translateX(-50%);
background: radial-gradient(ellipse, rgba(18, 60, 50, 0.3), transparent 70%); filter: blur(7px); z-index: -1; background: radial-gradient(ellipse, rgba(18, 60, 50, 0.3), transparent 70%); filter: blur(7px); z-index: -1;
@@ -245,9 +271,9 @@ h1, h2, h3 { text-wrap: balance; margin: 0; }
.hero-grid { grid-template-columns: 1fr; gap: 44px; } .hero-grid { grid-template-columns: 1fr; gap: 44px; }
.hero-visual { order: 2; } .hero-visual { order: 2; }
.hero-copy { order: 1; } .hero-copy { order: 1; }
.book3d { --bw: 256px; --bh: 358px; --bt: 48px; } .book3d { --bw: 256px; --bh: 358px; --bt: 48px; --band-h: 50px; }
} }
@media (max-width: 420px) { .book3d { --bw: 210px; --bh: 294px; --bt: 40px; } } @media (max-width: 420px) { .book3d { --bw: 210px; --bh: 294px; --bt: 40px; --band-h: 40px; } }
@media (prefers-reduced-motion: reduce) { .book3d__inner { animation: none; } } @media (prefers-reduced-motion: reduce) { .book3d__inner { animation: none; } }
/* ============ FIELD NOTES (even gutters, aligned to container edges) ============ */ /* ============ FIELD NOTES (even gutters, aligned to container edges) ============ */
@@ -268,7 +294,7 @@ h1, h2, h3 { text-wrap: balance; margin: 0; }
/* ============ RECIPE (value centerpiece) ============ */ /* ============ RECIPE (value centerpiece) ============ */
.recipe-head { max-width: 40rem; margin-bottom: 44px; } .recipe-head { max-width: 40rem; margin-bottom: 44px; }
.recipe-head h2 { font-family: var(--font-display); font-weight: 900; font-size: clamp(2rem, 4.3vw, 3.3rem); letter-spacing: -0.025em; line-height: 1.02; margin: 16px 0 18px; } .recipe-head h2 { font-family: var(--font-display); font-weight: 900; font-size: clamp(2rem, 4.3vw, 3.3rem); letter-spacing: -0.025em; line-height: 1.15; margin: 16px 0 18px; }
.recipe-head p { color: var(--color-muted); font-size: 1.08rem; } .recipe-head p { color: var(--color-muted); font-size: 1.08rem; }
.recipe { background: var(--color-card); border: 1px solid var(--color-line); border-radius: 20px; box-shadow: var(--sh-lg); overflow: hidden; } .recipe { background: var(--color-card); border: 1px solid var(--color-line); border-radius: 20px; box-shadow: var(--sh-lg); overflow: hidden; }
.recipe-top { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 20px 28px; border-bottom: 1px solid var(--color-line); background: linear-gradient(var(--color-card), var(--color-paper)); } .recipe-top { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 20px 28px; border-bottom: 1px solid var(--color-line); background: linear-gradient(var(--color-card), var(--color-paper)); }
@@ -314,7 +340,7 @@ h1, h2, h3 { text-wrap: balance; margin: 0; }
/* ============ CONTENTS (table-of-contents) ============ */ /* ============ CONTENTS (table-of-contents) ============ */
.contents { background: var(--color-paper-2); border-top: 1px solid var(--color-line); border-bottom: 1px solid var(--color-line); } .contents { background: var(--color-paper-2); border-top: 1px solid var(--color-line); border-bottom: 1px solid var(--color-line); }
.contents h2 { font-family: var(--font-display); font-weight: 900; font-size: clamp(1.9rem, 4vw, 3rem); letter-spacing: -0.025em; margin: 14px 0 34px; max-width: 20ch; } .contents h2 { font-family: var(--font-display); font-weight: 900; font-size: clamp(1.9rem, 4vw, 3rem); letter-spacing: -0.025em; line-height: 1.2; margin: 14px 0 34px; max-width: 20ch; }
.toc { border-top: 1px solid var(--color-line-2); } .toc { border-top: 1px solid var(--color-line-2); }
.toc a { display: grid; grid-template-columns: 52px 1fr auto auto; align-items: center; gap: 20px; padding: 22px 8px; border-bottom: 1px solid var(--color-line); transition: background 0.2s, padding 0.2s; } .toc a { display: grid; grid-template-columns: 52px 1fr auto auto; align-items: center; gap: 20px; padding: 22px 8px; border-bottom: 1px solid var(--color-line); transition: background 0.2s, padding 0.2s; }
.toc a:hover { background: var(--color-pine-wash); padding-left: 16px; } .toc a:hover { background: var(--color-pine-wash); padding-left: 16px; }
@@ -339,7 +365,7 @@ h1, h2, h3 { text-wrap: balance; margin: 0; }
@media (max-width: 600px) { .letter-in { padding: 0 20px; } } @media (max-width: 600px) { .letter-in { padding: 0 20px; } }
/* ============ AUTHORS ============ */ /* ============ AUTHORS ============ */
.authors h2 { font-family: var(--font-display); font-weight: 900; font-size: clamp(1.9rem, 3.6vw, 2.7rem); letter-spacing: -0.025em; margin: 14px 0 10px; } .authors h2 { font-family: var(--font-display); font-weight: 900; font-size: clamp(1.9rem, 3.6vw, 2.7rem); letter-spacing: -0.025em; line-height: 1.2; margin: 14px 0 10px; }
.authors .lede { color: var(--color-muted); max-width: 38rem; margin-bottom: 38px; } .authors .lede { color: var(--color-muted); max-width: 38rem; margin-bottom: 38px; }
.auth-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; } .auth-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.auth { background: var(--color-card); border: 1px solid var(--color-line); border-radius: 16px; padding: 24px; box-shadow: var(--sh-sm); transition: box-shadow 0.2s, transform 0.2s; } .auth { background: var(--color-card); border: 1px solid var(--color-line); border-radius: 16px; padding: 24px; box-shadow: var(--sh-sm); transition: box-shadow 0.2s, transform 0.2s; }
@@ -359,7 +385,7 @@ h1, h2, h3 { text-wrap: balance; margin: 0; }
/* ============ EMAIL CAPTURE ============ */ /* ============ EMAIL CAPTURE ============ */
.capture { background: var(--color-paper-2); border-top: 1px solid var(--color-line); border-bottom: 1px solid var(--color-line); } .capture { background: var(--color-paper-2); border-top: 1px solid var(--color-line); border-bottom: 1px solid var(--color-line); }
.capture-in { max-width: 640px; margin: 0 auto; text-align: center; padding: 0 28px; } .capture-in { max-width: 640px; margin: 0 auto; text-align: center; padding: 0 28px; }
.capture h2 { font-family: var(--font-display); font-weight: 900; font-size: clamp(2rem, 4vw, 3rem); letter-spacing: -0.025em; line-height: 1.02; margin: 16px 0 14px; } .capture h2 { font-family: var(--font-display); font-weight: 900; font-size: clamp(2rem, 4vw, 3rem); letter-spacing: -0.025em; line-height: 1.15; margin: 16px 0 14px; }
.capture p { color: var(--color-muted); font-size: 1.06rem; margin-bottom: 28px; } .capture p { color: var(--color-muted); font-size: 1.06rem; margin-bottom: 28px; }
.capture form { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; } .capture form { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.capture .fine { font-family: var(--font-mono); font-size: 11.5px; color: var(--color-muted); margin-top: 18px; } .capture .fine { font-family: var(--font-mono); font-size: 11.5px; color: var(--color-muted); margin-top: 18px; }
@@ -368,7 +394,7 @@ h1, h2, h3 { text-wrap: balance; margin: 0; }
/* ============ TIERS PREVIEW ============ */ /* ============ TIERS PREVIEW ============ */
.tiers-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; margin-bottom: 34px; } .tiers-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; margin-bottom: 34px; }
.tiers-head h2 { font-family: var(--font-display); font-weight: 900; font-size: clamp(1.9rem, 3.6vw, 2.7rem); letter-spacing: -0.025em; margin: 12px 0 0; } .tiers-head h2 { font-family: var(--font-display); font-weight: 900; font-size: clamp(1.9rem, 3.6vw, 2.7rem); letter-spacing: -0.025em; line-height: 1.2; margin: 12px 0 0; }
.tiers-note { font-family: var(--font-mono); font-size: 12px; color: var(--color-muted); max-width: 22rem; text-align: right; line-height: 1.6; } .tiers-note { font-family: var(--font-mono); font-size: 12px; color: var(--color-muted); max-width: 22rem; text-align: right; line-height: 1.6; }
.tier-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; } .tier-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.tier { background: var(--color-card); border: 1px solid var(--color-line); border-radius: 18px; padding: 30px 26px; display: flex; flex-direction: column; transition: box-shadow 0.2s, transform 0.2s; } .tier { background: var(--color-card); border: 1px solid var(--color-line); border-radius: 18px; padding: 30px 26px; display: flex; flex-direction: column; transition: box-shadow 0.2s, transform 0.2s; }
@@ -386,7 +412,7 @@ h1, h2, h3 { text-wrap: balance; margin: 0; }
@media (max-width: 820px) { .tier-grid { grid-template-columns: 1fr; } .tiers-note { text-align: left; } } @media (max-width: 820px) { .tier-grid { grid-template-columns: 1fr; } .tiers-note { text-align: left; } }
/* ============ FAQ ============ */ /* ============ FAQ ============ */
.faq h2 { font-family: var(--font-display); font-weight: 900; font-size: clamp(1.9rem, 3.6vw, 2.7rem); letter-spacing: -0.025em; margin-bottom: 34px; } .faq h2 { font-family: var(--font-display); font-weight: 900; font-size: clamp(1.9rem, 3.6vw, 2.7rem); letter-spacing: -0.025em; line-height: 1.2; margin-bottom: 34px; }
.qa { border-top: 1px solid var(--color-line); } .qa { border-top: 1px solid var(--color-line); }
.qa details { border-bottom: 1px solid var(--color-line); padding: 6px 0; } .qa details { border-bottom: 1px solid var(--color-line); padding: 6px 0; }
.qa summary { cursor: pointer; list-style: none; padding: 20px 0; display: flex; align-items: center; justify-content: space-between; gap: 20px; font-family: var(--font-display); font-weight: 600; font-size: 1.2rem; letter-spacing: -0.02em; } .qa summary { cursor: pointer; list-style: none; padding: 20px 0; display: flex; align-items: center; justify-content: space-between; gap: 20px; font-family: var(--font-display); font-weight: 600; font-size: 1.2rem; letter-spacing: -0.02em; }
@@ -408,3 +434,446 @@ h1, h2, h3 { text-wrap: balance; margin: 0; }
.foot-links a:hover { color: #f7f5f0; } .foot-links a:hover { color: #f7f5f0; }
.colophon { padding-top: 26px; font-family: var(--font-mono); font-size: 11.5px; color: #7a7266; letter-spacing: 0.02em; line-height: 1.8; } .colophon { padding-top: 26px; font-family: var(--font-mono); font-size: 11.5px; color: #7a7266; letter-spacing: 0.02em; line-height: 1.8; }
@media (max-width: 600px) { .foot-in { padding: 48px 20px 40px; } } @media (max-width: 600px) { .foot-in { padding: 48px 20px 40px; } }
/* ============ READER DEMO ============ */
/* Layout the landing page does not already have. Reader pages use a wider wrap.
Same tokens, same ~20px cards and 999px pills. Do not restyle the homepage recipe. */
.demo-chip {
display: inline-flex; align-items: center; gap: 8px;
font-family: var(--font-mono); font-size: 11px; font-weight: 500;
letter-spacing: 0.12em; text-transform: uppercase;
color: var(--color-amber);
background: rgba(180, 121, 31, 0.08);
border: 1px solid rgba(180, 121, 31, 0.22);
padding: 5px 12px; border-radius: 999px;
margin-bottom: 22px;
}
.reader-nav .nav-in,
.reader-footer .foot-in,
.wrap.reader-wide {
max-width: min(1520px, calc(100vw - 40px));
}
.crumbs {
padding: 4px 0 18px;
font-size: 13.5px;
}
.crumbs ol {
display: flex; flex-wrap: wrap; align-items: center;
list-style: none; margin: 0; padding: 0;
}
.crumbs li { display: flex; align-items: center; }
.crumbs-sep {
margin: 0 10px; color: var(--color-muted); font-weight: 400;
}
.crumbs a { color: var(--color-pine); font-weight: 600; }
.crumbs a:hover { color: var(--color-pine-deep); }
.crumbs [aria-current="page"] { color: var(--color-ink); font-weight: 600; }
.reader-nav .nav-title {
font-size: 14.5px; font-weight: 600; color: var(--color-ink);
}
.reader-nav .nav-title:hover { color: var(--color-pine); }
.reader-nav .reader-links { gap: 22px; }
.reader-nav .nav-current {
font-size: 14.5px; font-weight: 600; color: var(--color-ink);
max-width: 22ch; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
@media (max-width: 860px) {
.reader-nav .nav-links {
display: flex; align-items: center; gap: 16px; font-size: 13.5px;
}
.reader-nav .nav-cta {
display: inline-flex; font-size: 13px; padding: 8px 14px;
}
.reader-nav .nav-current { display: none; }
.reader-nav .nav-toggle { display: none; }
}
.chapter-page { padding: 48px 0 96px; }
.chapter-shell {
display: grid;
grid-template-columns: 240px minmax(0, 1fr);
gap: 32px 40px;
align-items: start;
}
@media (min-width: 1080px) {
.chapter-shell {
grid-template-columns: 240px minmax(0, 1fr) 280px;
}
}
.chapter-col { min-width: 0; }
.chapter-kicker { margin: 8px 0 14px; }
.chapter-title {
font-family: var(--font-display); font-weight: 900;
font-size: clamp(2.1rem, 4.6vw, 3.4rem);
letter-spacing: -0.028em; line-height: 1.15; margin: 0 0 12px;
max-width: 22ch;
}
.chapter-by {
font-family: var(--font-mono); font-size: 12.5px; color: var(--color-muted);
letter-spacing: 0.04em; margin-bottom: 20px;
}
.chapter-promise {
font-size: 1.12rem; line-height: 1.62; color: var(--color-ink); margin: 0 0 28px;
max-width: 46rem;
}
.chapter-outline {
position: sticky;
top: 68px;
z-index: 30;
align-self: start;
background: var(--color-paper);
border-right: 1px solid var(--color-line);
padding: 16px 20px 28px 0;
max-height: calc(100vh - 68px);
overflow-y: auto;
}
.outline-progress {
font-family: var(--font-mono); font-size: 11.5px; color: var(--color-muted);
letter-spacing: 0.04em; margin: 0 0 14px;
}
.outline-heading {
font-family: var(--font-display); font-weight: 600;
font-size: 1.08rem; letter-spacing: -0.02em; margin: 0 0 16px;
}
.outline-group { margin: 0 0 18px; }
.outline-group:last-child { margin-bottom: 0; }
.outline-group-label { margin: 0 0 8px; }
.outline-group ul { list-style: none; margin: 0; padding: 0; }
.outline-group li { margin: 0; }
.outline-group a {
display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
padding: 5px 0 5px 10px;
border-left: 2px solid transparent;
font-size: 13.5px; line-height: 1.35; color: var(--color-muted);
}
.outline-group a:hover { color: var(--color-ink); }
.outline-group a.is-active {
color: var(--color-pine); border-left-color: var(--color-pine); font-weight: 600;
}
.outline-done {
font-family: var(--font-mono); font-size: 12px; color: var(--color-pine); flex: none;
}
.chapter-rail {
position: sticky;
top: 84px;
align-self: start;
display: none;
}
@media (min-width: 1080px) {
.chapter-rail { display: block; }
}
.chapter-rail .spec { margin: 0 0 14px; }
.rail-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.rail-card {
display: flex; flex-direction: column; gap: 6px;
padding: 16px 18px;
background: var(--color-card);
border: 1px solid var(--color-line);
border-radius: 16px;
box-shadow: var(--sh-sm);
transition: border-color 0.18s, transform 0.14s var(--ease), box-shadow 0.18s;
}
.rail-card:hover {
border-color: var(--color-pine-tint);
transform: translateY(-1px);
box-shadow: var(--sh-md);
}
.rail-card.is-done { border-color: var(--color-pine-tint); background: var(--color-pine-wash); }
.rail-kicker {
font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.1em;
text-transform: uppercase; color: var(--color-pine);
}
.rail-title {
font-family: var(--font-display); font-weight: 600;
font-size: 1.15rem; letter-spacing: -0.02em; line-height: 1.2;
color: var(--color-ink);
}
.rail-meta {
font-family: var(--font-mono); font-size: 12px; color: var(--color-muted);
letter-spacing: 0.02em;
}
.rail-skip {
display: inline-block; margin-top: 16px;
font-family: var(--font-mono); font-size: 12.5px; font-weight: 600;
color: var(--color-pine);
}
.rail-skip:hover { color: var(--color-pine-deep); }
.chapter-outline-mobile {
margin: 0 0 24px;
padding: 10px 16px 12px;
border: 1px solid var(--color-line);
border-radius: 12px;
background: var(--color-card);
}
.chapter-outline-mobile summary {
cursor: pointer; list-style: none;
font-family: var(--font-display); font-weight: 600;
font-size: 1.05rem; letter-spacing: -0.02em;
padding: 6px 0;
}
.chapter-outline-mobile summary::-webkit-details-marker { display: none; }
.chapter-outline-mobile[open] summary { margin-bottom: 10px; }
.chapter-outline-mobile .outline-progress { margin-top: 4px; }
.chapter-kit {
margin: 0 0 28px;
padding: 18px 20px 16px;
background: var(--color-pine-wash);
border: 1px solid var(--color-pine-tint);
border-radius: 16px;
}
.chapter-kit-title {
font-family: var(--font-display); font-weight: 600;
font-size: 1.2rem; letter-spacing: -0.02em;
margin: 0 0 6px;
}
.chapter-kit-lede {
font-size: 14.5px; color: var(--color-muted);
margin: 0 0 16px;
}
.kit-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px 28px;
margin: 0;
}
@media (min-width: 900px) {
.kit-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
.kit-grid { grid-template-columns: 1fr; }
}
.kit-grid dt {
font-family: var(--font-mono); font-size: 11px; font-weight: 500;
letter-spacing: 0.12em; text-transform: uppercase;
color: var(--color-pine); margin: 0 0 6px;
}
.kit-grid dd { margin: 0; font-size: 14.5px; line-height: 1.4; color: var(--color-ink); }
.kit-grid ul { list-style: none; margin: 0; padding: 0; }
.kit-grid li { margin: 0 0 4px; }
.kit-grid li:last-child { margin-bottom: 0; }
.workflow-kit {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
gap: 12px 16px;
margin: 4px 0 8px;
padding: 14px 0 16px;
border-bottom: 1px dashed var(--color-line-2);
}
.workflow-kit dt {
font-family: var(--font-mono); font-size: 11px; font-weight: 500;
letter-spacing: 0.1em; text-transform: uppercase;
color: var(--color-pine); margin: 0 0 4px;
}
.workflow-kit dd {
margin: 0; font-size: 14px; line-height: 1.4; color: var(--color-ink);
}
.rail-kit {
font-size: 12.5px; line-height: 1.35; color: var(--color-muted);
}
.rail-out { color: var(--color-ink); }
.chapter-map {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 40px;
margin: 0 0 8px;
padding: 18px 0 6px;
border-top: 1px solid var(--color-line);
}
.chapter-map-title { grid-column: 1 / -1; }
@media (max-width: 720px) {
.chapter-map { grid-template-columns: 1fr; }
}
.chapter-map-title {
font-family: var(--font-display); font-weight: 600;
font-size: 1.2rem; letter-spacing: -0.02em;
margin: 0 0 16px;
}
.chapter-map-block { margin: 0 0 16px; }
.chapter-map-block .spec { margin: 0 0 8px; }
.chapter-map-block ul { margin: 0; padding: 0 0 0 1.1em; }
.chapter-map-block li { margin: 0 0 6px; }
.chapter-map-block a { color: var(--color-pine); font-weight: 600; }
.chapter-map-block a:hover { color: var(--color-pine-deep); }
.skip-row {
display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
margin: 0 0 48px; padding: 16px 0;
border-top: 1px dashed var(--color-line-2);
border-bottom: 1px dashed var(--color-line-2);
}
.skip-label {
font-family: var(--font-mono); font-size: 11.5px; font-weight: 500;
letter-spacing: 0.12em; text-transform: uppercase; color: var(--color-pine);
margin-right: 4px;
}
.skip-chip {
display: inline-flex; align-items: baseline; gap: 8px;
font-family: var(--font-mono); font-size: 12.5px; color: var(--color-pine);
background: var(--color-pine-wash); border: 1px solid var(--color-pine-tint);
padding: 7px 13px; border-radius: 999px;
transition: background 0.18s, transform 0.14s var(--ease);
}
.skip-chip:hover { background: var(--color-pine-tint); transform: translateY(-1px); }
.skip-chip:focus-visible { outline: 2px solid var(--color-pine); outline-offset: 3px; }
.skip-chip-job {
font-family: var(--font-body); font-size: 13px; font-weight: 600;
color: var(--color-ink); letter-spacing: 0; text-transform: none;
}
.essay,
.recipe-nested,
.end-matter,
.end-sources,
.recipe-nested .row {
scroll-margin-top: 88px;
}
.essay { margin: 0 0 36px; max-width: 46rem; }
.essay h2 {
font-family: var(--font-display); font-weight: 600;
font-size: 1.45rem; letter-spacing: -0.02em; line-height: 1.2;
margin: 0 0 12px;
}
.essay p { color: var(--color-ink); margin: 0 0 14px; }
.essay p:last-child { margin-bottom: 0; }
.workflow-kicker { margin: 8px 0 10px; }
.recipe-nested {
margin: 0 0 52px;
}
.recipe-nested .step-copy { min-width: 0; flex: 1; }
.recipe-nested .step-copy .term { margin-top: 12px; }
.recipe-nested h4.k { margin: 0; font-weight: 500; }
.recipe-nested .term {
overflow-x: auto;
overflow-y: hidden;
}
.recipe-nested .term pre { overflow-x: auto; }
.recipe-jump {
display: flex; flex-wrap: wrap; gap: 8px 16px;
margin: 0 0 8px; padding: 0 0 16px;
border-bottom: 1px dashed var(--color-line-2);
font-size: 13.5px;
}
.recipe-jump a { color: var(--color-pine); font-weight: 600; }
.recipe-jump a:hover { color: var(--color-pine-deep); }
.recipe-jump a:focus-visible { outline: 2px solid var(--color-pine); outline-offset: 2px; }
.plain-list { margin: 0; padding: 0 0 0 1.1em; }
.plain-list li { margin: 0 0 6px; }
.plain-list li:last-child { margin-bottom: 0; }
.evidence-p { margin: 0 0 10px; }
.evidence-p:last-child { margin-bottom: 0; }
.sub-line { color: var(--color-muted); font-size: 14.5px; margin: 8px 0 12px; }
.inline-sample {
font-family: var(--font-mono); font-size: 13px; color: var(--color-pine);
border-bottom: 1px solid var(--color-pine-tint);
}
.inline-sample:hover { color: var(--color-pine-deep); }
.tryit-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
.tryit-actions .btn { font-size: 14px; padding: 11px 18px; }
.tryit-done { gap: 8px; }
.recipe-record {
display: flex; flex-direction: column; gap: 6px;
font-size: 14.5px; color: var(--color-muted);
}
.recipe-record a { color: var(--color-pine); font-weight: 600; }
.recipe-record a:hover { color: var(--color-pine-deep); }
.end-matter {
margin-top: 56px; padding-top: 28px;
border-top: 1px solid var(--color-line);
}
.end-sources { margin-top: 32px; }
.end-matter h2,
.end-sources h2 {
font-family: var(--font-display); font-weight: 600;
font-size: 1.35rem; letter-spacing: -0.02em; margin: 0 0 12px;
}
.end-matter h2 + .further-list { margin-bottom: 8px; }
.further-list { margin: 0 0 8px; padding: 0 0 0 1.1em; color: var(--color-ink); }
.further-list li { margin: 0 0 8px; }
.further-list a { color: var(--color-pine); font-weight: 600; }
.chapter-updated {
font-family: var(--font-mono); font-size: 12px; color: var(--color-muted);
letter-spacing: 0.04em; margin-top: 18px;
}
.library-title {
font-family: var(--font-display); font-weight: 900;
font-size: clamp(1.9rem, 4vw, 3rem); letter-spacing: -0.025em;
line-height: 1.15; margin: 14px 0 16px;
white-space: nowrap;
}
.library-lede { color: var(--color-muted); max-width: 38rem; margin-bottom: 34px; }
.toc .toc-soon {
display: grid; grid-template-columns: 52px 1fr auto auto;
align-items: center; gap: 20px; padding: 22px 8px;
border-bottom: 1px solid var(--color-line);
color: var(--color-muted);
}
.toc .soon {
font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--color-muted);
background: var(--color-card); border: 1px solid var(--color-line);
padding: 4px 10px; border-radius: 999px; white-space: nowrap;
}
.reader-library .toc a {
grid-template-columns: 52px 1fr auto auto auto;
}
.toc-count {
font-family: var(--font-mono); font-size: 11.5px; font-weight: 500;
letter-spacing: 0.08em; text-transform: uppercase;
color: var(--color-pine); white-space: nowrap;
}
@media (max-width: 720px) {
.toc .toc-soon .desc { display: none; }
.toc .toc-soon { grid-template-columns: 40px 1fr auto; }
.reader-library .toc a { grid-template-columns: 40px 1fr auto auto; }
}
.recipe-focus-page { padding: 40px 0 96px; }
.recipe-focus-page .demo-chip { margin-bottom: 12px; }
.recipe-focus-page .recipe { max-width: none; }
.focus-back {
font-family: var(--font-mono); font-size: 13px; color: var(--color-muted);
margin: 0 0 28px;
}
.focus-back a { color: var(--color-pine); font-weight: 600; }
@media (max-width: 959px) {
.chapter-shell { grid-template-columns: 1fr; gap: 0; }
.chapter-outline { display: none; }
.chapter-rail { display: none; }
}
@media (min-width: 960px) {
.chapter-outline-mobile { display: none; }
}
@media print {
.reader-nav, .chapter-outline, .chapter-outline-mobile, .chapter-rail, .reader-footer, .skip-row, .tryit-actions, .demo-chip, .recipe-jump {
display: none !important;
}
.recipe-nested { break-inside: avoid; box-shadow: none; }
}
+224
View File
@@ -0,0 +1,224 @@
---
status: draft-for-review
type: chapter-template
edition: 2026
vertical: # real-estate | financial-advisors | fractional-c-suite | higher-ed | communities | technology
title: ""
authors: []
recipe_ids: [XX-01, XX-02]
last_updated: 2026-08-31
---
# Chapter structure (for review)
This is the blank chapter, matching PRD §5.4 (locked 2026-08-31). Review the shape before anyone writes a real vertical.
**How to use it.** Copy this file to `workflows/[vertical-slug].md` (example: `workflows/real-estate.md`). Replace `XX` with the vertical prefix. Leave this file as the blank.
**What a chapter is.** A substantial essay for one vertical. Full workflows sit inside the argument, at the point they earn, not in an appendix and not as a stack of cards.
**What a workflow is.** The unit of practice. Read it, run the Try-it (~15 min), share the result. That Try-it is what counts as activation.
**Rules that do not move.**
- Skip-to-Try-it at the top. Activation does not wait for the essay.
- Safeguards before Method. Data handling is read before anyone pastes a client list.
- Method shows real tool output, not a description of it.
- Nested recipes stay open. Do not hide Method behind "click to reveal."
- Every recipe has a stable ID and a heading it can link to.
- Every recipe names its kit (who runs it, tools, files, model, time, cost, artifact). The chapter page shows the union at the top and the kit on each nested recipe.
- No chatbot on the page. No crypto or Web3 language.
- PDF must match this stack: essay + in-place recipes.
**Vertical prefixes**
| Vertical | Slug | ID prefix |
|:--|:--|:--|
| Real Estate | `real-estate` | `RE` |
| Financial Advisors | `financial-advisors` | `FA` |
| Fractional C-Suite | `fractional-c-suite` | `CS` |
| Higher Ed | `higher-ed` | `HE` |
| Communities | `communities` | `CM` |
| Technology | `technology` | `TE` |
Fill every `[bracket]`. Two nested recipes are shown so the essay-wraps-workflow pattern is obvious. Add more at the points in the argument they belong. Delete the unused slot rather than leaving a hollow block.
---
# [Chapter title]
[Vertical] · 2026 edition · [Author names]
## What this chapter is for
[One short promise. The job this vertical's reader is trying to get done this week, and what they will be able to run by the end of the chapter.]
**Skip to a Try-it:** [XX-01](#xx-01) · [XX-02](#xx-02)
## What you need
The chapter page shows this before the essay. Union of the nested workflows. Named tools, sample files, Try-it time, sample-run cost. No live client data.
- Tools: [named]
- Files: [named samples we ship]
- Model: [named, or "the model you already use"]
- Time: [each Try-it]
- Cost: [sample-run total, or "unknown"]
## [Essay: the stakes]
[Why this work is hard here. Specific to the vertical, not a generic AI preamble. Name the actual failure: a listing that stalls, a client file that leaks, a committee that will not move.]
## [Essay: constraints]
[What you cannot ignore in this vertical. Tools people already use. Compliance, clients, time. What "done" looks like for them.]
## [Essay: how we work here]
[The practice this chapter argues for. Then the first workflow, at the moment the argument needs proof.]
---
<a id="xx-01"></a>
### [Job title. This is the workflow title, written as the job.]
**XX-01** · v0.1 · ~[N] min · ~$[cost or "free with tools you already have"]
#### Kit
Shown at the top of the nested recipe. Inventory, not instructions. Setup below still explains how to use these.
- You run: [role]
- Tools: [named tools, versions if it matters]
- Files: [sample files we ship]
- Model: [named, or "the model you already use"]
- Time: ~[N] min
- Cost: [amount or "free with tools you already have"]
- You produce: [artifact]
#### Problem
[The real task. One job, not a category.]
#### Actors
- Who runs it: [role]
- Who is affected: [client, student, team]
- What the model is allowed to do: [draft / classify / retrieve]. What a human must still do: [send / approve / file]
#### Setup
- Tools: [named tools, versions if it matters]
- Sample files: [what we ship with this recipe, or "use the anonymized packet below"]
- Cost: [model/tool cost if known. If unknown, say so.]
#### Safeguards
Read this before the steps.
- Data handling: [what never goes into the prompt. What is ok if stripped.]
- Human oversight: [the checkpoint before anything leaves the building]
- Failure modes: [how this goes wrong in this vertical]
- Escalation: [who you call, and when you stop]
#### Method
Ordered steps. Paste the actual tool output inside the step that produced it. A screenshot or a quoted block beats a paraphrase.
1. [Step]
```
[real output]
```
2. [Step]
3. [Step]
#### Evidence
[What "it worked" looks like. An artifact, a number, a before/after. Not "the user felt more confident."]
#### Try it (~15 min)
- Sample data: [what they use so they do not need a live client]
- What to produce: [the artifact]
- Done when: [a check they can mark. Completing this is activation.]
#### Share the result
[What they post: the outcome, not the workflow text. Point at the community template when it exists. Events, not content.]
**Author:** [name] · **Sources:** [links or citations] · **Last updated:** [date]
---
## [Essay continues]
[The argument picks back up. What the first recipe proved, what it did not. Then the next workflow, when the next claim needs proof.]
---
<a id="xx-02"></a>
### [Second job title]
**XX-02** · v0.1 · ~[N] min · ~$[cost]
#### Kit
Shown at the top of the nested recipe. Inventory, not instructions. Setup below still explains how to use these.
- You run: [role]
- Tools: [named tools, versions if it matters]
- Files: [sample files we ship]
- Model: [named, or "the model you already use"]
- Time: ~[N] min
- Cost: [amount or "free with tools you already have"]
- You produce: [artifact]
#### Problem
[ ]
#### Actors
[ ]
#### Setup
[ ]
#### Safeguards
[ ]
#### Method
1. [ ]
2. [ ]
#### Evidence
[ ]
#### Try it (~15 min)
[ ]
#### Share the result
[ ]
**Author:** [name] · **Sources:** [ ] · **Last updated:** [date]
---
## Further workflows in this vertical
- [XX-01](#xx-01) — [job title]
- [XX-02](#xx-02) — [job title]
- [Later IDs, once assigned]
## Sources
[Chapter-level sources. Recipe-level citations stay on the recipe.]
**Last updated:** 2026-08-31