fix: TLS fail-closed, PORT validation, startup error detection, force-headers, sync metadata

ober

13b8bfd910d449aa52ab1e050687a741656d88b4

diff --git a/secure-site.ss b/secure-site.ss
index f2d9d1e..46c4d28 100644
--- a/secure-site.ss
+++ b/secure-site.ss
@@ -1,20 +1,55 @@
 (import (only (std misc thread) thread-sleep!)
+        (std net httpsd)
         (sinatra)
+        (sinatra handler)
         (sinatra security))
 
 (set-option! "environment" "production")
 (set-option! "static" #f)
 
+(define site-asset-root (or (getenv "JERBOA_SITE_ASSET_ROOT") "."))
+
+(define (site-asset-path path)
+  (string-append site-asset-root "/" path))
+
+(define (read-text-asset path)
+  (call-with-input-file path get-string-all))
+
+(define repl-css (read-text-asset (site-asset-path "assets/repl/repl.css")))
+(define repl-js (read-text-asset (site-asset-path "assets/repl/repl.js")))
+(define repl-worker-js (read-text-asset (site-asset-path "assets/repl/repl-worker.js")))
+(define repl-manifest-json (read-text-asset (site-asset-path "assets/repl/manifest.json")))
+(define repl-wasm
+  (call-with-port
+    (open-file-input-port (site-asset-path "assets/repl/jerboa-repl.wasm") (file-options) 'block #f)
+    get-bytevector-all))
+(define favicon-ico
+  (call-with-port
+    (open-file-input-port (site-asset-path "assets/favicon.ico") (file-options) 'block #f)
+    get-bytevector-all))
+
 (define logo-uri
   "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAyNCIgaGVpZ2h0PSIxMDI0IiB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHJvbGU9ImltZyIgYXJpYS1sYWJlbGxlZGJ5PSJ0aXRsZSBkZXNjIj4KICA8dGl0bGUgaWQ9InRpdGxlIj5KZXJib2EgbGFuZ3VhZ2UgbG9nbzwvdGl0bGU+CiAgPGRlc2MgaWQ9ImRlc2MiPkEgY2FydG9vbiBqZXJib2EgbWFzY290IHdpdGggbGFyZ2UgZWFycywgbG9uZyBoaW5kIGxlZ3MsIGEgY3VydmVkIHRhaWwsIGEgbGFtYmRhIGJhZGdlLCBhbmQgdGhlIEplcmJvYSB3b3JkbWFyay48L2Rlc2M+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImZ1ck1haW4iIHgxPSIzMDIiIHkxPSIyNDgiIHgyPSI3MjAiIHkyPSI3NTQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZGZmNGZmIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMC40OCIgc3RvcC1jb2xvcj0iIzhiYjlkNiIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM1MjcxOGYiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImZ1ckRhcmsiIHgxPSI1MDgiIHkxPSIyMTQiIHgyPSI3NTIiIHkyPSI3MTAiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjNmY5ZWMyIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzJmNGM2NSIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYmVsbHkiIHgxPSIzOTIiIHkxPSI0MjEiIHgyPSI2MDAiIHkyPSI3MjkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZjRmYmZmIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2M2ZTNmMyIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZWFySW5uZXIiIHgxPSIzNDQiIHkxPSIxMTEiIHgyPSI2ODAiIHkyPSIzNTUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZGNlY2ZmIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzhjYWFkMiIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxmaWx0ZXIgaWQ9InNvZnRTaGFkb3ciIHg9Ii0yMCUiIHk9Ii0yMCUiIHdpZHRoPSIxNDAlIiBoZWlnaHQ9IjE0MCUiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CiAgICAgIDxmZURyb3BTaGFkb3cgZHg9IjAiIGR5PSIxOCIgc3RkRGV2aWF0aW9uPSIxOCIgZmxvb2QtY29sb3I9IiMwYjFkMmMiIGZsb29kLW9wYWNpdHk9IjAuMiIvPgogICAgPC9maWx0ZXI+CiAgICA8c3R5bGU+CiAgICAgIC5vdXRsaW5lIHsgc3Ryb2tlOiAjMTMyMzM0OyBzdHJva2Utd2lkdGg6IDE4OyBzdHJva2UtbGluZWNhcDogcm91bmQ7IHN0cm9rZS1saW5lam9pbjogcm91bmQ7IH0KICAgICAgLmRldGFpbCB7IHN0cm9rZTogIzEzMjMzNDsgc3Ryb2tlLXdpZHRoOiAxMjsgc3Ryb2tlLWxpbmVjYXA6IHJvdW5kOyBzdHJva2UtbGluZWpvaW46IHJvdW5kOyB9CiAgICAgIC5maW5lIHsgc3Ryb2tlOiAjMTMyMzM0OyBzdHJva2Utd2lkdGg6IDg7IHN0cm9rZS1saW5lY2FwOiByb3VuZDsgc3Ryb2tlLWxpbmVqb2luOiByb3VuZDsgfQogICAgPC9zdHlsZT4KICA8L2RlZnM+CgogIDxwYXRoIGQ9Ik0xOTkgODc1YzcyLTMzIDU1MC0zMyA2MjIgMCIgY2xhc3M9ImRldGFpbCIgb3BhY2l0eT0iMC4xNiIvPgoKICA8ZyBmaWx0ZXI9InVybCgjc29mdFNoYWRvdykiPgogICAgPHBhdGggZD0iTTcwOCA1NzVjNTcgMjIgMTExIDMyIDE1OSAyMSA2My0xNCA5MC01OCA3Ni0xMDAtMTItMzctNTUtNDktOTItMjgtMjUgMTQtNDMgNDMtMzUgNzIgNiAyMiAyNiAzNCA1MCAzMyIgY2xhc3M9ImRldGFpbCIgc3Ryb2tlPSIjMTMyMzM0Ii8+CiAgICA8cGF0aCBkPSJNNzA5IDU3NGM2MyAzMSAxMTkgNDMgMTY0IDI4IDU0LTE4IDc1LTYwIDYzLTk2LTEwLTI5LTQ1LTM4LTc0LTIyLTIwIDExLTMzIDM0LTI4IDUzIDQgMTcgMTkgMjQgMzYgMjMiIHN0cm9rZT0iI2M4ZTZmNiIgc3Ryb2tlLXdpZHRoPSIzMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CgogICAgPHBhdGggZD0iTTU0OCAxOTFjMjItNzMgNzctMTI5IDEyMy0xMjcgNDEgMiA1NyA1MSAzNCAxMTItMjAgNTQtNzEgMTExLTEyOSAxNDMiIGZpbGw9InVybCgjZnVyTWFpbikiIGNsYXNzPSJvdXRsaW5lIi8+CiAgICA8cGF0aCBkPSJNNTgyIDIwM2MxOC00NSA1Mi04MiA3OS04NiAyMi0zIDMwIDIwIDE4IDU1LTEyIDM1LTQ3IDc0LTg0IDk4IiBmaWxsPSJ1cmwoI2VhcklubmVyKSIgY2xhc3M9ImZpbmUiLz4KCiAgICA8cGF0aCBkPSJNNDMwIDIwNWMtMzItNzEtODgtMTE4LTEzMS0xMTAtNDAgNy00OCA1OC0xOCAxMTUgMjYgNTEgODMgMTAxIDE0NSAxMjQiIGZpbGw9InVybCgjZnVyTWFpbikiIGNsYXNzPSJvdXRsaW5lIi8+CiAgICA8cGF0aCBkPSJNMzk3IDIxOWMtMjMtNDMtNjEtNzQtODgtNzQtMjIgMS0yNyAyNS0xMSA1NyAxNiAzMyA1NSA2NyA5NSA4NSIgZmlsbD0idXJsKCNlYXJJbm5lcikiIGNsYXNzPSJmaW5lIi8+CgogICAgPHBhdGggZD0iTTYxNiA2NjljNTAgNDUgMTAyIDc2IDE2MiA3NSA0Mi0xIDY4IDE5IDY4IDQ3IDAgMzUtNDMgNTctOTcgNDUtNjgtMTUtMTIwLTU4LTE2Mi0xMjYiIGZpbGw9InVybCgjZnVyTWFpbikiIGNsYXNzPSJvdXRsaW5lIi8+CiAgICA8cGF0aCBkPSJNNjU1IDcyNGMzMyAyNiA2OCA0MCAxMDUgMzkgMjgtMSA0NiA5IDQ3IDI1IDEgMjAtMjggMzAtNjYgMjEtNDAtOS03OC0zOC0xMTMtODUiIGZpbGw9IiNkOWVmZmEiIGNsYXNzPSJmaW5lIi8+CgogICAgPHBhdGggZD0iTTM3NCA2NDFjLTYxIDM3LTEwOSA4Ni0xMzUgMTUxLTE4IDQ2LTU1IDU4LTgzIDM2LTM1LTI3LTIyLTgzIDI5LTEyMSA1NC00MSAxMDktNzQgMTY0LTk5IiBmaWxsPSJ1cmwoI2Z1ck1haW4pIiBjbGFzcz0ib3V0bGluZSIvPgogICAgPHBhdGggZD0iTTMxNyA2ODNjLTM2IDI4LTYzIDYzLTgxIDEwNS0xMCAyNC0yOCAzMi00MiAyMi0xOS0xNC0xMC00MyAyMi02OCAzNi0yNyA3Mi00OSAxMDgtNjYiIGZpbGw9IiNkOWVmZmEiIGNsYXNzPSJmaW5lIi8+CgogICAgPHBhdGggZD0iTTU3NSAzNjVjODQgNDAgMTQxIDEyNCAxMzkgMjE4LTQgMTM1LTkzIDIyMy0yMjEgMjE5LTEyNi01LTIxNy05OC0yMTEtMjI1IDQtOTIgNjEtMTc0IDE0NS0yMTIiIGZpbGw9InVybCgjZnVyTWFpbikiIGNsYXNzPSJvdXRsaW5lIi8+CiAgICA8cGF0aCBkPSJNNDEwIDQ2OWMtNTAgNTEtNzAgMTQyLTM5IDIxMSAyOCA2MiA3NiA5NSAxMzMgOTcgNjEgMiAxMTMtMzAgMTQzLTkyIDMyLTY5IDgtMTY0LTQxLTIxNi0yMiA3MS02MyAxMDktMTAwIDEwOS0zNiAwLTc0LTM3LTk2LTEwOVoiIGZpbGw9InVybCgjYmVsbHkpIiBjbGFzcz0iZGV0YWlsIi8+CgogICAgPHBhdGggZD0iTTUxMyAxODljMTAwIDAgMTc0IDcyIDE3NCAxNzQgMCAxMDQtNzYgMTg0LTE3NyAxODQtMTA2IDAtMTg0LTc2LTE4NC0xODEgMC0xMDUgNzgtMTc3IDE4Ny0xNzdaIiBmaWxsPSJ1cmwoI2Z1ck1haW4pIiBjbGFzcz0ib3V0bGluZSIvPgogICAgPHBhdGggZD0iTTQ0OCA0MTRjMjEgMzkgMTA5IDQxIDEzMiAxIDMgNTEtMjEgODgtNjUgODktNDUgMS03Mi0zNi02Ny05MFoiIGZpbGw9IiNmNmZiZmYiIGNsYXNzPSJmaW5lIi8+CiAgICA8cGF0aCBkPSJNNTEyIDM4NmMxOCAwIDM0IDkgMzQgMjIgMCAxNC0xNyAzMC0zNCAzMC0xOCAwLTM0LTE2LTM0LTMwIDAtMTMgMTYtMjIgMzQtMjJaIiBmaWxsPSIjMTMyMzM0Ii8+CiAgICA8cGF0aCBkPSJNNTAzIDQzNWMtMTQgMTgtMzEgMjYtNTEgMjYiIGNsYXNzPSJmaW5lIi8+CiAgICA8cGF0aCBkPSJNNTIyIDQzNWMxNSAxOCAzMiAyNiA1MiAyNiIgY2xhc3M9ImZpbmUiLz4KCiAgICA8Y2lyY2xlIGN4PSI0MzgiIGN5PSIzMzUiIHI9IjI0IiBmaWxsPSIjMTMyMzM0Ii8+CiAgICA8Y2lyY2xlIGN4PSI1ODUiIGN5PSIzMzUiIHI9IjI0IiBmaWxsPSIjMTMyMzM0Ii8+CiAgICA8Y2lyY2xlIGN4PSI0NDciIGN5PSIzMjYiIHI9IjciIGZpbGw9IiNmOGZjZmYiLz4KICAgIDxjaXJjbGUgY3g9IjU5NCIgY3k9IjMyNiIgcj0iNyIgZmlsbD0iI2Y4ZmNmZiIvPgogICAgPHBhdGggZD0iTTM0OSAzNzFjLTM4LTE1LTc1LTEzLTExMSA0IiBjbGFzcz0iZmluZSIvPgogICAgPHBhdGggZD0iTTM1MCA0MDdjLTQzIDAtNzkgMTEtMTEwIDM1IiBjbGFzcz0iZmluZSIvPgogICAgPHBhdGggZD0iTTY3NSAzNjljMzgtMTYgNzUtMTYgMTEyIDAiIGNsYXNzPSJmaW5lIi8+CiAgICA8cGF0aCBkPSJNNjc2IDQwNWM0NC0yIDgxIDggMTEzIDMxIiBjbGFzcz0iZmluZSIvPgoKICAgIDxwYXRoIGQ9Ik0zOTkgNTYwYy01MCA1LTg0IDM0LTEwNyA3NyIgY2xhc3M9ImRldGFpbCIvPgogICAgPHBhdGggZD0iTTYyNiA1NjBjNDkgNSA4MiAzMyAxMDQgNzQiIGNsYXNzPSJkZXRhaWwiLz4KICAgIDxwYXRoIGQ9Ik0zNjcgNTc1Yy0xOCAyMi0yOSA0NS0zMyA3MCIgY2xhc3M9ImZpbmUiLz4KICAgIDxwYXRoIGQ9Ik02NTkgNTc1YzE4IDIyIDI5IDQ1IDMzIDcwIiBjbGFzcz0iZmluZSIvPgoKICAgIDxwYXRoIGQ9Ik0zMTUgNDg5Yy0zNi0yLTU4IDE1LTY3IDUwIDI1LTE0IDUwLTE4IDc1LTkiIGZpbGw9InVybCgjZnVyTWFpbikiIGNsYXNzPSJvdXRsaW5lIi8+CiAgICA8cGF0aCBkPSJNNzA4IDQ4OWMzNi0yIDU4IDE1IDY3IDUwLTI1LTE0LTUwLTE4LTc1LTkiIGZpbGw9InVybCgjZnVyTWFpbikiIGNsYXNzPSJvdXRsaW5lIi8+CgogICAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjE5IDYyNykgcm90YXRlKC0xMykiPgogICAgICA8Y2lyY2xlIGN4PSIwIiBjeT0iMCIgcj0iNzAiIGZpbGw9IiNlZWY4ZmYiIGNsYXNzPSJkZXRhaWwiLz4KICAgICAgPHRleHQgeD0iMCIgeT0iMzkiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJHZW9yZ2lhLCBUaW1lcyBOZXcgUm9tYW4sIHNlcmlmIiBmb250LXNpemU9IjExMiIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzEzMjMzNCIgc3Ryb2tlPSIjMTMyMzM0IiBzdHJva2Utd2lkdGg9IjIiPiYjOTU1OzwvdGV4dD4KICAgIDwvZz4KICA8L2c+CgogIDxnIGFyaWEtbGFiZWw9IkplcmJvYSI+CiAgICA8dGV4dCB4PSI1MTIiIHk9Ijk1NCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkF2ZW5pciBOZXh0LCBUcmVidWNoZXQgTVMsIEFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzNiIgZm9udC13ZWlnaHQ9IjkwMCIgbGV0dGVyLXNwYWNpbmc9IjAiIGZpbGw9IiMxMzIzMzQiIHN0cm9rZT0iI2U5ZjZmZiIgc3Ryb2tlLXdpZHRoPSIxMiIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgcGFpbnQtb3JkZXI9InN0cm9rZSBmaWxsIj5KZXJib2E8L3RleHQ+CiAgPC9nPgogIDxwYXRoIGQ9Ik0yMzUgNzg0Yy00OCAyNi03NSA1OC03NSA5NiAwIDQ2IDQwIDgzIDExMiAxMTAiIGNsYXNzPSJkZXRhaWwiIG9wYWNpdHk9IjAuMzgiLz4KICA8cGF0aCBkPSJNNzg5IDc4NGM0OCAyNiA3NSA1OCA3NSA5NiAwIDQ2LTQwIDgzLTExMiAxMTAiIGNsYXNzPSJkZXRhaWwiIG9wYWNpdHk9IjAuMzgiLz4KPC9zdmc+Cg==")
 
 (define site-css
-  "html{font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;color:#2c3e50;background:#fff}body{margin:0;font-size:16px;line-height:1.7}a{color:#2f8f68;text-decoration:none}a:hover{text-decoration:underline}.navbar{position:sticky;top:0;z-index:10;height:3.6rem;background:#fff;border-bottom:1px solid #eaecef;display:flex;align-items:center;justify-content:space-between;padding:0 1.5rem;box-sizing:border-box}.brand{display:flex;align-items:center;color:#2c3e50;font-size:1.3rem;font-weight:650}.brand-mark{width:2.25rem;height:2.25rem;margin-right:.7rem}.nav{display:flex;gap:1.1rem;align-items:center;font-size:.95rem}.nav-link{color:#3a5169}.nav-link.active{color:#2f8f68;font-weight:650}.repo-link{border:1px solid #d4dde6;border-radius:6px;padding:.25rem .65rem;color:#3a5169}.home{max-width:1080px;margin:0 auto;padding:0 2rem}.hero{text-align:center;padding:2.1rem 0 1rem}.hero img{display:block;max-width:270px;width:54vw;max-height:270px;margin:0 auto 1.1rem}.hero h1{font-size:3.25rem;line-height:1.08;margin:.7rem 0;color:#1f2d3a;letter-spacing:0}.description{max-width:43rem;margin:1rem auto 1.5rem;font-size:1.45rem;line-height:1.35;color:#5e7891}.actions{display:flex;gap:.8rem;justify-content:center;flex-wrap:wrap}.action-button{display:inline-block;background:#2f9f74;color:#fff;padding:.72rem 1.35rem;border-radius:4px;border-bottom:1px solid #267e5d;font-size:1.08rem}.action-button:hover{background:#37ad80;text-decoration:none}.secondary-button{display:inline-block;color:#3a5169;border:1px solid #cfd8e3;padding:.72rem 1.1rem;border-radius:4px}.secondary-button:hover{text-decoration:none;border-color:#91a4b7}.features{border-top:1px solid #eaecef;margin-top:2.3rem;padding:1.4rem 0 0;display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between}.feature{flex:1 1 30%;max-width:30%;padding-bottom:1.4rem}.feature h2{font-size:1.35rem;font-weight:560;color:#3a5169;margin:.75rem 0 .25rem}.feature p,.feature li{color:#4e6e8e}.feature ul{padding-left:1.2rem;margin:.45rem 0 0}.content{border-top:1px solid #eaecef;margin-top:2.2rem;padding:2rem 0 3rem}.content h1{font-size:2.3rem;line-height:1.16;color:#1f2d3a;margin:0 0 1rem}.content h2{font-size:1.45rem;color:#3a5169;margin:2rem 0 .45rem}.lead{font-size:1.25rem;color:#5e7891;max-width:56rem}.columns{display:grid;grid-template-columns:repeat(3,1fr);gap:1.1rem;margin-top:1.3rem}.panel{border:1px solid #e1e7ee;border-radius:6px;padding:1rem;background:#fff}.panel h3{margin:.1rem 0 .4rem;color:#2c3e50}.panel p{margin:.35rem 0;color:#4e6e8e}.panel ul{margin:.35rem 0;padding-left:1.15rem;color:#4e6e8e}.code{background:#282c34;color:#f8f8f2;border-radius:6px;padding:1rem 1.2rem;overflow:auto;line-height:1.45;font-family:\"SFMono-Regular\",Consolas,monospace;font-size:.92rem}.muted{color:#6f8194}.footer{border-top:1px solid #eaecef;color:#6f8194;text-align:center;padding:2rem;margin-top:1.2rem}.route-list{display:grid;grid-template-columns:repeat(2,1fr);gap:1rem}.repo-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.9rem;margin-top:1rem}.repo-card{border:1px solid #e1e7ee;border-radius:6px;padding:.9rem;background:#fff;min-height:10.5rem;display:flex;flex-direction:column}.repo-card h3{font-size:1.05rem;margin:.2rem 0 .35rem;color:#223344;overflow-wrap:anywhere}.repo-card p{margin:.2rem 0 .7rem;color:#4e6e8e;line-height:1.45}.repo-card a,.repo-card .muted{margin-top:auto;font-size:.9rem}.repo-count{display:flex;gap:.6rem;flex-wrap:wrap;margin:1.2rem 0}.repo-count span{border:1px solid #d7e0e9;border-radius:6px;padding:.3rem .65rem;color:#3a5169;background:#fbfdff}.label{font-size:.78rem;text-transform:uppercase;color:#7b8da0;letter-spacing:.05em}.badges{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.65rem}.badge{border:1px solid #d7e0e9;border-radius:999px;padding:.15rem .55rem;color:#4e6e8e;font-size:.86rem}@media(max-width:900px){.repo-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:760px){.navbar{height:auto;min-height:3.6rem;align-items:flex-start;gap:.5rem;flex-direction:column;padding:.7rem 1rem}.nav{width:100%;overflow:auto;padding-bottom:.2rem}.home{padding:0 1.1rem}.hero{padding-top:1.3rem}.hero h1{font-size:2.35rem}.description{font-size:1.15rem}.features{display:block}.feature{max-width:100%}.columns,.route-list,.repo-grid{grid-template-columns:1fr}.content h1{font-size:1.9rem}}")
+  (string-append
+    ":root{--ink:#18212b;--muted:#607080;--line:#d9e2ea;--paper:#fbf8f0;--page:#f5f7f8;--panel:#ffffff;--blue:#2457c5;--cyan:#0b7890;--green:#217857;--red:#b54c2f;--gold:#b78016;--shadow:0 18px 50px rgba(24,33,43,.12)}"
+    "*{box-sizing:border-box}html{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;color:var(--ink);background:var(--page)}body{margin:0;font-size:16px;line-height:1.65;background:radial-gradient(circle at top left,#e7f5f5 0,#f5f7f8 24rem),linear-gradient(180deg,#fff 0,#f5f7f8 42rem);min-width:320px}a{color:var(--blue);text-decoration:none}a:hover{text-decoration:underline}pre{margin:0;white-space:pre;overflow:auto}"
+    ".navbar{position:sticky;top:0;z-index:10;min-height:4.25rem;background:rgba(255,255,255,.91);border-bottom:1px solid rgba(217,226,234,.9);backdrop-filter:saturate(160%) blur(18px);display:flex;align-items:center;justify-content:space-between;padding:.75rem clamp(1rem,3vw,2.4rem);gap:1rem}.brand{display:flex;align-items:center;color:var(--ink);font-size:1.08rem;font-weight:800}.brand-mark{width:2.55rem;height:2.55rem;margin-right:.7rem}.nav{display:flex;gap:.35rem;align-items:center;font-size:.93rem;white-space:nowrap}.nav-link,.repo-link{color:#33465a;border-radius:6px;padding:.45rem .64rem}.nav-link.active{color:var(--ink);background:#eaf2f4;font-weight:750}.repo-link{border:1px solid var(--line);background:#fff}"
+    ".home{max-width:1180px;margin:0 auto;padding:0 clamp(1rem,4vw,2.4rem)}.hero{min-height:calc(100vh - 4.25rem);display:grid;grid-template-columns:minmax(0,1.08fr) minmax(22rem,.92fr);gap:clamp(2rem,5vw,4.5rem);align-items:center;padding:clamp(2.2rem,5vw,5.2rem) 0}.hero-copy{max-width:48rem}.eyebrow{display:inline-flex;align-items:center;gap:.5rem;font-size:.78rem;font-weight:850;text-transform:uppercase;letter-spacing:.08em;color:var(--cyan);border:1px solid #b9dbe2;background:#f4ffff;border-radius:999px;padding:.35rem .7rem}.hero h1{font-size:clamp(3.05rem,9vw,7rem);line-height:.92;margin:.9rem 0 1.1rem;color:#101923;letter-spacing:0}.description{font-size:clamp(1.2rem,2vw,1.55rem);line-height:1.36;color:#46586a;max-width:43rem;margin:0 0 1.4rem}.actions{display:flex;gap:.8rem;flex-wrap:wrap;margin:1.4rem 0}.action-button,.secondary-button{display:inline-flex;align-items:center;justify-content:center;min-height:2.8rem;border-radius:6px;padding:.72rem 1rem;font-weight:800}.action-button{background:var(--ink);color:#fff;box-shadow:0 8px 22px rgba(24,33,43,.2)}.action-button:hover{background:#283847;text-decoration:none}.secondary-button{color:var(--ink);border:1px solid #c9d5de;background:#fff}.secondary-button:hover{text-decoration:none;border-color:#9fb0bf}.metric-row{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:.75rem;max-width:42rem;margin-top:1.5rem}.metric{border-top:2px solid #c9d5de;padding-top:.7rem}.metric strong{display:block;font-size:1.55rem;line-height:1;color:var(--ink)}.metric span{display:block;color:var(--muted);font-size:.9rem;margin-top:.3rem}"
+    ".hero-visual{position:relative;min-height:32rem}.logo-plate{position:absolute;right:0;top:0;width:min(23rem,82vw);aspect-ratio:1;border:1px solid #cfdae2;border-radius:8px;background:linear-gradient(150deg,#fff,#edf7f8);box-shadow:var(--shadow);display:grid;place-items:center}.logo-plate img{width:72%;height:72%;object-fit:contain}.terminal-card{position:absolute;left:0;bottom:0;width:min(31rem,92vw);border:1px solid #bfd0da;border-radius:8px;background:#fffdf7;box-shadow:var(--shadow);overflow:hidden}.terminal-bar{display:flex;align-items:center;gap:.45rem;border-bottom:1px solid #e2d9c6;background:#f1eadb;padding:.62rem .8rem;color:#6d5f48;font-size:.84rem;font-weight:760}.dot{width:.62rem;height:.62rem;border-radius:999px;background:#d6654b}.dot:nth-child(2){background:#d99c2b}.dot:nth-child(3){background:#3c9d72}.terminal-body{padding:1rem 1.05rem;font-family:\"SFMono-Regular\",ui-monospace,Consolas,monospace;font-size:.92rem;line-height:1.62;color:#22313d}.prompt{color:#9a5d14}.cmd{color:#143a54;font-weight:750}.out{color:#5e6e7b}.panel-band{padding:clamp(2rem,5vw,4.5rem) 0;border-top:1px solid var(--line)}.section-head{display:flex;align-items:end;justify-content:space-between;gap:1.5rem;margin-bottom:1.2rem}.section-head h2,.content h1{font-size:clamp(2rem,4vw,3.6rem);line-height:1;margin:0;color:#101923;letter-spacing:0}.section-head p,.lead{font-size:1.12rem;color:#536577;max-width:46rem;margin:.55rem 0 0}.feature-grid,.columns{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1rem}.feature,.panel{border:1px solid var(--line);border-radius:8px;background:rgba(255,255,255,.82);padding:1.05rem}.feature h3,.panel h3{margin:.15rem 0 .45rem;color:#172230;font-size:1.08rem}.feature p,.feature li,.panel p,.panel li{color:#4c5f71;margin:.35rem 0}.panel ul,.feature ul{padding-left:1.15rem;margin:.35rem 0}.content{padding:clamp(2.2rem,5vw,4.5rem) 0 4rem}.content h2{font-size:1.55rem;color:#172230;margin:2rem 0 .55rem}.content h1{margin-bottom:.9rem}.code{border:1px solid #d8ccb8;border-radius:8px;background:#fffaf0;color:#24313d;padding:1rem 1.1rem;overflow:auto;line-height:1.55;font-family:\"SFMono-Regular\",ui-monospace,Consolas,monospace;font-size:.92rem;box-shadow:inset 0 1px 0 #fff}.code.shell{background:#fbf3df}.code.scheme{background:#f8fbff;border-color:#cdddea}.kw{color:#7a3db2;font-weight:750}.fn{color:#1b62b7}.str{color:#9a5d14}.sym{color:#147157}.com{color:#7d8a96}.num{color:#a64733}.muted{color:var(--muted)}.route-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem;margin-top:1.2rem}.label{font-size:.73rem;text-transform:uppercase;color:#7b8da0;letter-spacing:.07em;font-weight:850}.badges,.repo-count{display:flex;gap:.5rem;flex-wrap:wrap}.badge,.repo-count span{border:1px solid #cfdbe4;border-radius:999px;padding:.2rem .6rem;color:#44596c;background:#fff;font-size:.86rem}.repo-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:.85rem;margin-top:1rem}.repo-card{border:1px solid var(--line);border-radius:8px;padding:.95rem;background:#fff;min-height:10.2rem;display:flex;flex-direction:column}.repo-card h3{font-size:1.03rem;margin:.2rem 0 .35rem;color:#172230;overflow-wrap:anywhere}.repo-card p{margin:.2rem 0 .8rem;color:#4e6072;line-height:1.45}.repo-card a,.repo-card .muted{margin-top:auto;font-size:.9rem}.footer{border-top:1px solid var(--line);color:#6f8194;text-align:center;padding:2rem;margin-top:1.2rem}"
+    ".terminal-card{border-color:#e35d36;background:#fff7e6;box-shadow:0 22px 55px rgba(20,47,74,.18),0 0 0 6px rgba(255,180,68,.16)}.terminal-bar{background:linear-gradient(90deg,#ffe08a,#ffd0a8 48%,#c7f3ef);border-bottom-color:#f0a43a;color:#533916}.terminal-body{background:linear-gradient(135deg,#fffaf0 0,#f2fbff 54%,#fff1f4 100%);color:#16283a;font-weight:650}.prompt{color:#d13b2f;font-weight:900}.cmd{color:#005ec4;font-weight:900}.out{color:#11735f}.code{border-color:#f09a38;background:linear-gradient(135deg,#fff7dc 0,#f2fbff 50%,#fff2f7 100%);color:#17202a;box-shadow:inset 0 1px 0 #fff,0 12px 30px rgba(18,45,68,.09)}.code.shell{background:linear-gradient(135deg,#fff1bf 0,#e9fbff 56%,#f7ecff 100%);border-color:#e8832f}.code.scheme{background:linear-gradient(135deg,#f5f8ff 0,#effff8 52%,#fff1d6 100%);border-color:#56add9}.kw{color:#8a19bd;font-weight:900}.fn{color:#005fd1;font-weight:900}.str{color:#c24d00}.sym{color:#00855f;font-weight:800}.com{color:#607489;font-style:italic}.num{color:#d12652;font-weight:850}"
+    "@media(max-width:980px){.hero{grid-template-columns:1fr;min-height:auto}.hero-visual{min-height:30rem}.logo-plate{right:0}.feature-grid,.columns,.repo-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.section-head{display:block}}@media(max-width:720px){.navbar{position:static;align-items:flex-start;flex-direction:column}.nav{width:100%;overflow:auto;padding-bottom:.15rem}.hero h1{font-size:3.2rem}.metric-row,.feature-grid,.columns,.route-list,.repo-grid{grid-template-columns:1fr}.hero-visual{min-height:28rem}.logo-plate{width:18rem}.terminal-card{width:100%}.content h1,.section-head h2{font-size:2.25rem}}"))
+
+(define normal-content-security-policy
+  "default-src 'self'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; object-src 'none'; script-src 'none'; style-src 'self'; img-src 'self' data:")
 
-(define site-security-headers
+(define repl-content-security-policy
+  "default-src 'self'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; object-src 'none'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; style-src 'self'; img-src 'self' data:")
+
+(define site-forced-security-headers
   (list
     (cons "Strict-Transport-Security" "max-age=31536000; includeSubDomains")
-    (cons "Content-Security-Policy" "default-src 'self'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; object-src 'none'; script-src 'none'; style-src 'self'; img-src 'self' data:")
     (cons "X-Content-Type-Options" "nosniff")
     (cons "X-Frame-Options" "DENY")
     (cons "Referrer-Policy" "no-referrer")
@@ -22,7 +57,33 @@
     (cons "Cross-Origin-Opener-Policy" "same-origin")
     (cons "Cross-Origin-Resource-Policy" "same-origin")))
 
-(set-option! "force-headers" site-security-headers)
+;; Force the site security headers on EVERY final response: the framework applies
+;; force-headers last (after routing), so no route can skip them — in particular
+;; the REPL assets, whose repl-headers carry only Content-Type/CSP/Cache-Control
+;; and would otherwise miss HSTS, X-Frame-Options, nosniff, etc.
+(set-option! "force-headers" site-forced-security-headers)
+
+(define (site-string-prefix? prefix text)
+  (let ((prefix-length (string-length prefix)))
+    (and (<= prefix-length (string-length text))
+         (string=? prefix (substring text 0 prefix-length)))))
+
+(define (repl-path? path)
+  (or (string=? path "/repl/")
+      (site-string-prefix? "/assets/repl/" path)))
+
+(define (repl-headers content-type)
+  (list
+    (cons "Content-Type" content-type)
+    (cons "Content-Security-Policy" repl-content-security-policy)
+    (cons "Cache-Control" "no-store")))
+
+(define (normal-headers content-type)
+  (append
+    site-forced-security-headers
+    (list
+      (cons "Content-Type" content-type)
+      (cons "Content-Security-Policy" normal-content-security-policy))))
 
 (define install-script
   (string-append
@@ -38,12 +99,36 @@
                  (if (string=? key active) " active" "")
                  "\" href=\"" href "\">" label "</a>"))
 
+(define (code class body)
+  (string-append "<div class=\"code " class "\"><pre>" body "</pre></div>"))
+
+(define hero-terminal
+  (string-append
+    "<div class=\"terminal-card\"><div class=\"terminal-bar\"><span class=\"dot\"></span><span class=\"dot\"></span><span class=\"dot\"></span><span>jerboa build</span></div>"
+    "<div class=\"terminal-body\"><pre><span class=\"prompt\">$</span> <span class=\"cmd\">jerboa pkg verify</span>\n<span class=\"out\">manifest ok / capabilities declared / hashes pinned</span>\n<span class=\"prompt\">$</span> <span class=\"cmd\">jerboa jerbuild binary service.ss dist/service</span>\n<span class=\"out\">native image written: dist/service</span></pre></div></div>"))
+
+(define sample-service
+  (code
+    "scheme"
+    "<span class=\"kw\">(import</span> <span class=\"sym\">(jerboa prelude)</span>\n        <span class=\"sym\">(std net httpsd)</span>\n        <span class=\"sym\">(std json)</span><span class=\"kw\">)</span>\n\n<span class=\"kw\">(def</span> <span class=\"fn\">(handler req)</span>\n  <span class=\"kw\">(respond-json</span> <span class=\"num\">200</span>\n    <span class=\"str\">'{status: \"ok\" runtime: \"native\"}</span><span class=\"kw\">))</span>\n\n<span class=\"kw\">(def</span> <span class=\"fn\">(main)</span>\n  <span class=\"kw\">(httpsd-start</span> <span class=\"num\">8443</span> handler <span class=\"str\">\"fullchain.pem\"</span> <span class=\"str\">\"privkey.pem\"</span><span class=\"kw\">))</span>"))
+
+(define build-commands
+  (code
+    "shell"
+    "<span class=\"prompt\">$</span> <span class=\"cmd\">jerboa app.ss</span>\n<span class=\"prompt\">$</span> <span class=\"cmd\">jerboa jerbuild transpile src lib --force</span>\n<span class=\"prompt\">$</span> <span class=\"cmd\">jerboa jerbuild exec --libdirs lib app.ss</span>\n<span class=\"prompt\">$</span> <span class=\"cmd\">jerboa jerbuild binary --libdirs lib app.ss dist/app</span>"))
+
+(define package-commands
+  (code
+    "shell"
+    "<span class=\"prompt\">$</span> <span class=\"cmd\">jerboa pkg init</span>\n<span class=\"prompt\">$</span> <span class=\"cmd\">jerboa pkg pack</span>\n<span class=\"prompt\">$</span> <span class=\"cmd\">jerboa pkg verify</span>\n<span class=\"prompt\">$</span> <span class=\"cmd\">jerboa pkg install</span>"))
+
 (define (layout title active body)
   (string-append
     "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">"
     "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
     "<title>" title " | Jerboa</title>"
     "<meta name=\"description\" content=\"Jerboa language, runtime, package, and documentation site.\">"
+    "<link rel=\"icon\" href=\"/favicon.ico\" sizes=\"any\">"
     "<link rel=\"stylesheet\" href=\"/assets/site.css\"></head><body>"
     "<header class=\"navbar\"><a class=\"brand\" href=\"/\">"
     "<img class=\"brand-mark\" src=\"" logo-uri "\" alt=\"Jerboa logo\"><span>Jerboa</span></a>"
@@ -51,6 +136,7 @@
     (nav-link "home" "/" "Home" active)
     (nav-link "guide" "/guide/" "Guide" active)
     (nav-link "docs" "/docs/" "Docs" active)
+    (nav-link "repl" "/repl/" "REPL" active)
     (nav-link "packages" "/packages/" "Packages" active)
     (nav-link "repos" "/repos/" "Repos" active)
     (nav-link "security" "/security/" "Security" active)
@@ -64,32 +150,72 @@
     "home"
     (string-append
       "<section class=\"hero\" aria-labelledby=\"main-title\">"
-      "<img src=\"" logo-uri "\" alt=\"Jerboa mascot logo\">"
+      "<div class=\"hero-copy\"><span class=\"eyebrow\">Native Scheme systems language</span>"
       "<h1 id=\"main-title\">Jerboa</h1>"
-      "<p class=\"description\">A secure, native, batteries-included language and runtime for building static binaries, network services, tools, and packages.</p>"
-      "<p class=\"actions\"><a class=\"action-button\" href=\"/guide/\">Get Started</a><a class=\"secondary-button\" href=\"/docs/\">Read the Docs</a></p>"
+      "<p class=\"description\">A secure, batteries-included language for building native binaries, network services, package ecosystems, and serious tools without dragging a runtime zoo behind every program.</p>"
+      "<p class=\"actions\"><a class=\"action-button\" href=\"/guide/\">Start Building</a><a class=\"secondary-button\" href=\"/repos/\">Explore Projects</a></p>"
+      "<div class=\"metric-row\"><div class=\"metric\"><strong>66</strong><span>Jerboa project repos tracked</span></div><div class=\"metric\"><strong>0</strong><span>browser scripts required</span></div><div class=\"metric\"><strong>1</strong><span>command surface for build, pkg, LSP, MCP</span></div></div></div>"
+      "<div class=\"hero-visual\"><div class=\"logo-plate\"><img src=\"" logo-uri "\" alt=\"Jerboa mascot logo\"></div>" hero-terminal "</div>"
       "</section>"
-      "<section class=\"features\" aria-label=\"Jerboa features\">"
-      "<div class=\"feature\"><h2>Built for Programs That Ship</h2><p>Jerboa has a whole-program binary path, cross-build support, and a standard library shaped for real services.</p></div>"
-      "<div class=\"feature\"><h2>Native Rust Boundary</h2><p>Crypto, TLS, HTTP parsing, regex, compression, and selected OS features live behind one audited Rust native backend.</p></div>"
-      "<div class=\"feature\"><h2>Security First</h2><ul><li>rustls HTTPSD</li><li>strict request framing</li><li>Landlock, seccomp, Capsicum, seatbelt</li></ul></div>"
-      "<div class=\"feature\"><h2>Web and Network Stack</h2><p>HTTP clients, HTTPS servers, fibers, WebSocket, DNS, SMTP, SSH, S3, gRPC, JSON-RPC, and routing are in-tree.</p></div>"
-      "<div class=\"feature\"><h2>Tooling and Packages</h2><p>One command dispatches the REPL, builder, package manager, MCP server, and LSP server.</p></div>"
-      "<div class=\"feature\"><h2>It Speaks Jerboa</h2><div class=\"code\"><pre>(import (jerboa prelude) (std net httpsd))\n\n(def (main)\n  (displayln \"ship it\"))</pre></div></div>"
+      "<section class=\"panel-band\" aria-label=\"Jerboa features\"><div class=\"section-head\"><div><span class=\"eyebrow\">What it is</span><h2>A language stack for programs that ship</h2></div><p>Jerboa is not just a syntax experiment. The project includes a compiler path, package manager, web framework, security tooling, editor support, network services, and native integration work.</p></div>"
+      "<div class=\"feature-grid\">"
+      "<div class=\"feature\"><h3>Native Deliverables</h3><p>Whole-program builds, static Linux artifacts, and direct service deployment are first-class outcomes.</p></div>"
+      "<div class=\"feature\"><h3>Audited Native Edge</h3><p>Crypto, TLS, HTTP parsing, regex, compression, and selected OS features live behind a focused Rust boundary.</p></div>"
+      "<div class=\"feature\"><h3>Security as Architecture</h3><ul><li>rustls HTTPSD</li><li>strict request framing</li><li>Landlock, seccomp, Capsicum, and seatbelt modules</li></ul></div>"
+      "<div class=\"feature\"><h3>Network Batteries</h3><p>HTTP clients, HTTPS servers, WebSocket, DNS, SMTP, SSH, S3, gRPC, JSON-RPC, routing, and fibers are part of the ecosystem.</p></div>"
+      "<div class=\"feature\"><h3>One Toolchain</h3><p>The Jerboa command surface covers REPL, builder, package manager, MCP server, LSP server, and project execution.</p></div>"
+      "<div class=\"feature\"><h3>Scheme With Teeth</h3>" sample-service "</div>"
+      "</div></section>"
+      "<section class=\"panel-band\" aria-label=\"Language priorities\"><div class=\"section-head\"><div><span class=\"eyebrow\">Why it exists</span><h2>Small core, serious boundaries, broad standard library</h2></div><p>The center of gravity is practical systems work: useful abstractions, explicit capabilities, deterministic packaging, and real deployment paths.</p></div>"
+      "<div class=\"columns\"><div class=\"panel\"><h3>For Services</h3><p>Build HTTPS endpoints, mail, DNS, search, browser-facing tools, and integration services with a runtime designed for network programs.</p></div><div class=\"panel\"><h3>For Tools</h3><p>Write shells, core utilities, Git helpers, editors, analysis tools, and local agents in the same language stack.</p></div><div class=\"panel\"><h3>For Research That Lands</h3><p>Explore actors, typed work, WASM boundaries, native Rust handoff, security monitoring, and compiler backends without splitting the project identity.</p></div></div>"
       "</section>")))
 
+(define repl-example
+  "(import (jerboa prelude))\n\n(def (square x)\n  (* x x))\n\n(displayln \"squares\")\n(map square '(1 2 3 4 5))\n")
+
+(define repl-page
+  (string-append
+    "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">"
+    "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
+    "<title>Jerboa Browser REPL | Jerboa</title>"
+    "<meta name=\"description\" content=\"Jerboa browser subset REPL.\">"
+    "<link rel=\"stylesheet\" href=\"/assets/site.css\">"
+    "<link rel=\"stylesheet\" href=\"/assets/repl/v1/repl.css\">"
+    "<scr" "ipt type=\"module\" src=\"/assets/repl/v1/repl.js\" defer></scr" "ipt>"
+    "</head><body>"
+    "<header class=\"navbar\"><a class=\"brand\" href=\"/\">"
+    "<img class=\"brand-mark\" src=\"" logo-uri "\" alt=\"Jerboa logo\"><span>Jerboa</span></a>"
+    "<nav class=\"nav\" aria-label=\"Main\">"
+    (nav-link "home" "/" "Home" "repl")
+    (nav-link "guide" "/guide/" "Guide" "repl")
+    (nav-link "docs" "/docs/" "Docs" "repl")
+    (nav-link "repl" "/repl/" "REPL" "repl")
+    (nav-link "packages" "/packages/" "Packages" "repl")
+    (nav-link "repos" "/repos/" "Repos" "repl")
+    (nav-link "security" "/security/" "Security" "repl")
+    "<a class=\"repo-link\" href=\"https://git.sr.ht/~lisp/jerboa\">Source</a>"
+    "</nav></header>"
+    "<main class=\"repl-shell\">"
+    "<section class=\"repl-head\" aria-labelledby=\"repl-title\"><div><h1 id=\"repl-title\">Jerboa Browser REPL</h1><span class=\"repl-subset\">subset</span></div><div class=\"repl-status\" data-repl-status role=\"status\" aria-live=\"polite\">Loading engine</div></section>"
+    "<noscript><span class=\"repl-noscript\">JavaScript is required for the browser REPL.</span></noscript>"
+    "<section class=\"repl-grid\">"
+    "<div class=\"repl-pane\"><div class=\"repl-toolbar\"><select data-repl-example aria-label=\"Example program\"></select><button type=\"button\" data-repl-load title=\"Load selected example\">Load</button><button type=\"button\" data-repl-run title=\"Run (Ctrl+Enter or Cmd+Enter)\">Run</button><button type=\"button\" data-repl-reset title=\"Reset session\">Reset</button><button type=\"button\" data-repl-clear title=\"Clear output\">Clear Output</button></div>"
+    "<textarea class=\"repl-editor\" data-repl-editor spellcheck=\"false\" autocapitalize=\"off\" autocomplete=\"off\" autocorrect=\"off\" aria-label=\"Jerboa source editor\">" repl-example "</textarea></div>"
+    "<div class=\"repl-pane\"><div class=\"repl-transcript\" data-repl-transcript role=\"log\" aria-label=\"REPL transcript\" aria-live=\"polite\"></div></div>"
+    "</section></main>"
+    "</body></html>"))
+
 (define guide-page
   (layout
     "Guide"
     "guide"
     (string-append
       "<section class=\"content\"><h1>Get Started</h1>"
-      "<p class=\"lead\">Jerboa uses one command for the language runtime, builder, packages, LSP, and service tooling.</p>"
-      "<h2>Install</h2><div class=\"code\"><pre>Network installer disabled pending a signed immutable release.</pre></div>"
+      "<p class=\"lead\">Jerboa uses one command for the language runtime, builder, packages, LSP, MCP, and service tooling. The public installer intentionally stays closed until release signing is provisioned.</p>"
+      "<h2>Install</h2>" (code "shell" "<span class=\"out\">Network installer disabled pending a signed immutable release.</span>")
       "<p>Until the project provisions an independently protected release signer, build from a reviewed local checkout. The install endpoint fails closed and never fetches mutable source.</p>"
-      "<h2>Run a file</h2><div class=\"code\"><pre>jerboa app.ss</pre></div>"
-      "<h2>Build a project</h2><div class=\"code\"><pre>jerboa jerbuild transpile src lib --force\njerboa jerbuild exec --libdirs lib app.ss\njerboa jerbuild binary --libdirs lib app.ss dist/app</pre></div>"
-      "<h2>Serve HTTPS</h2><div class=\"code\"><pre>(import (std net httpsd))\n\n(httpsd-start 8443 handler \"fullchain.pem\" \"privkey.pem\")</pre></div>"
+      "<h2>Run and Build</h2>" build-commands
+      "<h2>Serve HTTPS</h2>" (code "scheme" "<span class=\"kw\">(import</span> <span class=\"sym\">(std net httpsd)</span><span class=\"kw\">)</span>\n\n<span class=\"kw\">(httpsd-start</span> <span class=\"num\">8443</span> handler <span class=\"str\">\"fullchain.pem\"</span> <span class=\"str\">\"privkey.pem\"</span><span class=\"kw\">)</span>")
       "<div class=\"columns\"><div class=\"panel\"><h3>1. Write</h3><p>Use Jerboa modules, records, macros, fibers, HTTP handlers, and package manifests.</p></div>"
       "<div class=\"panel\"><h3>2. Verify</h3><p>Run project tests through Jerboa tooling and keep native boundaries explicit.</p></div>"
       "<div class=\"panel\"><h3>3. Ship</h3><p>Build a native binary or a static Linux artifact with Jerboa's binary pipeline.</p></div></div>"
@@ -101,12 +227,14 @@
     "docs"
     (string-append
       "<section class=\"content\"><h1>Documentation</h1>"
-      "<p class=\"lead\">Start with the language guide, then move into build, security, packages, native Rust integration, and web services.</p>"
+      "<p class=\"lead\">The canonical docs live in the Jerboa source tree. This site presents the launch surface and points directly to the language, build, security, package, native Rust, and web service references.</p>"
       "<div class=\"route-list\">"
       "<div class=\"panel\"><span class=\"label\">Language</span><h3>Jerboa Language</h3><p>Reader syntax, modules, macros, records, matching, typed work, and runtime conventions.</p><a href=\"https://git.sr.ht/~lisp/jerboa/tree/master/item/docs/JERBOA-LANG.md\">Open docs</a></div>"
       "<div class=\"panel\"><span class=\"label\">Build</span><h3>Single Binaries</h3><p>Whole-program builds, boot embedding, static Linux builds, and deployment notes.</p><a href=\"https://git.sr.ht/~lisp/jerboa/tree/master/item/docs/single-binary.md\">Open docs</a></div>"
       "<div class=\"panel\"><span class=\"label\">Security</span><h3>Hardening</h3><p>Use the secure native backend, capability modules, audit helpers, and binary hardening tools.</p><a href=\"/security/\">Security page</a></div>"
       "<div class=\"panel\"><span class=\"label\">Packages</span><h3>jpkg</h3><p>Secure package manifests, deterministic artifacts, capability declarations, and local links.</p><a href=\"/packages/\">Package page</a></div>"
+      "<div class=\"panel\"><span class=\"label\">Concurrency</span><h3>Fibers and Actors</h3><p>Lightweight concurrency, async work, actor model notes, and service runtime patterns.</p><a href=\"https://git.sr.ht/~lisp/jerboa/tree/master/item/docs/actor-model.md\">Open docs</a></div>"
+      "<div class=\"panel\"><span class=\"label\">Native</span><h3>Rust Boundary</h3><p>FFI, Rust target work, native library design, and secure protocol handoff.</p><a href=\"https://git.sr.ht/~lisp/jerboa/tree/master/item/docs/native-rust.md\">Open docs</a></div>"
       "</div></section>")))
 
 (define packages-page
@@ -116,7 +244,7 @@
     (string-append
       "<section class=\"content\"><h1>Packages</h1>"
       "<p class=\"lead\">Jerboa packages are explicit data: manifests, dependencies, capabilities, and deterministic build outputs.</p>"
-      "<div class=\"code\"><pre>jerboa pkg init\njerboa pkg pack\njerboa pkg verify\njerboa pkg install</pre></div>"
+      package-commands
       "<div class=\"columns\"><div class=\"panel\"><h3>Secure by Default</h3><p>Installs are data operations. Build and native privileges are declared instead of assumed.</p></div>"
       "<div class=\"panel\"><h3>Local Development</h3><p>Use local links for active packages while keeping publish and verify paths strict.</p></div>"
       "<div class=\"panel\"><h3>Curated Ecosystem</h3><p>Core packages live beside Jerboa projects for web, DNS, shell, editor, crypto, database, and network tools.</p></div></div>"
@@ -159,6 +287,7 @@
     (list "jerboa-lsp.retired" "Retired" "Retired LSP implementation kept for historical reference." (sourcehut-repo "jerboa-lsp"))
     (list "jerboa-treesitter" "Parser" "Tree-sitter grammar and parser tooling for Jerboa syntax." (sourcehut-repo "jerboa-treesitter"))
     (list "jerboa-search" "Search" "Local search and web extraction workbench." "")
+    (list "jerboa-git" "Git" "Git integration package and repository workflow experiments." "")
     (list "jerboa-gitlab" "DevOps" "GitLab integration and issue workflow tooling." (sourcehut-repo "jerboa-gitlab"))
     (list "jerboa-gitsafe" "Git" "Git safety tooling and repository guardrails." (sourcehut-repo "jerboa-gitsafe"))))
 
@@ -170,6 +299,7 @@
     (list "jerboa-awk" "CLI" "AWK-compatible text-processing work in Jerboa." (sourcehut-repo "jerboa-awk"))
     (list "jerboa-sed" "CLI" "Stream editing utilities and parser work." (sourcehut-repo "jerboa-sed"))
     (list "jerboa-top" "CLI" "Process and system monitor experiments." (sourcehut-repo "jerboa-top"))
+    (list "jerboa-term" "Terminal" "Terminal UI and terminal integration work." "")
     (list "jerboa-asm" "Systems" "Assembler and low-level code generation work." (sourcehut-repo "jerboa-asm"))
     (list "jerboa-fuse" "Filesystem" "FUSE integration and filesystem tooling." (sourcehut-repo "jerboa-fuse"))
     (list "jerboa-inotify" "Filesystem" "File notification bindings and watchers." (sourcehut-repo "jerboa-inotify"))))
@@ -184,6 +314,7 @@
     (list "jerboa-ssh" "SSH" "SSH client and protocol work." (sourcehut-repo "jerboa-ssh"))
     (list "jerboa-sshd" "SSH" "SSH server experiments and daemon integration." (sourcehut-repo "jerboa-sshd"))
     (list "jerboa-ssl" "TLS" "SSL/TLS compatibility and certificate tooling." (sourcehut-repo "jerboa-ssl"))
+    (list "jerboa-wireguard" "VPN" "WireGuard-oriented network integration work." "")
     (list "jerboa-wormhole" "Network" "Magic Wormhole style transfer and rendezvous tooling." (sourcehut-repo "jerboa-wormhole"))
     (list "jerboa-webex" "Web" "Web extraction, browser-facing, and endpoint integration work." (sourcehut-repo "jerboa-webex"))
     (list "jerboa-websearch" "Web" "Search, fetch, and extraction services." (sourcehut-repo "jerboa-websearch"))
@@ -202,6 +333,7 @@
     (list "jerboa-secmonlib" "Security" "Reusable monitoring and security library code." (sourcehut-repo "jerboa-secmonlib"))
     (list "jerboa-semgrep" "Security" "Semgrep-compatible analysis and rule-engine experiments." (sourcehut-repo "jerboa-semgrep"))
     (list "jerboa-signal" "Security" "Signal and secure messaging integration work." (sourcehut-repo "jerboa-signal"))
+    (list "jerboa-virii" "Security" "Security research and malware-analysis incubator work." "")
     (list "jerboa-virus" "Security" "Malware analysis and detection experiments." (sourcehut-repo "jerboa-virus"))))
 
 (define platform-repositories
@@ -219,9 +351,15 @@
     (list "jerboa-qt" "GUI" "Qt bindings and desktop application work." (sourcehut-repo "jerboa-qt"))
     (list "jerboa-scintilla" "GUI" "Scintilla editor component bindings." (sourcehut-repo "jerboa-scintilla"))
     (list "jerboa-ssd-recognizer" "Vision" "SSD recognition and vision pipeline experiments." (sourcehut-repo "jerboa-ssd-recognizer"))
+    (list "jerboa-tetris" "Demo" "Game and UI demo project for interactive Jerboa work." "")
     (list "jerboa-vision" "Vision" "Computer vision packages and examples." (sourcehut-repo "jerboa-vision"))
     (list "jerboa-wafter" "Build" "Build and workflow automation experiments." (sourcehut-repo "jerboa-wafter"))))
 
+(define local-repositories
+  (list
+    (list "jerboa-shell-backup-20260526.git" "Archive" "Local shell repository backup retained outside the public project catalog." "")
+    (list "jerboa-temp-dir" "Workspace" "Temporary local workspace directory tracked here for complete adjacent-project coverage." "")))
+
 (define related-repositories
   (list
     (list "kratistos" "Benchmark" "Agent benchmark harnesses and reproducible task runners used to harden Jerboa tooling." (sourcehut-repo "kratistos"))))
@@ -233,13 +371,14 @@
     (string-append
       "<section class=\"content\"><h1>Repository Atlas</h1>"
       "<p class=\"lead\">Jerboa is not a single repository; it is a language, runtime, package set, service stack, editor environment, and security research platform. This catalog lists the public Jerboa repositories in this checkout and related projects that show the scope of software being written with it.</p>"
-      "<div class=\"repo-count\"><span>59 Jerboa repositories</span><span>7 project areas</span><span>No secret repositories listed</span></div>"
+      "<div class=\"repo-count\"><span>66 Jerboa project directories</span><span>8 project areas</span><span>2 related ecosystem projects</span></div>"
       (repo-section "Core Language" "The language, runtime, website, and compatibility surface." core-repositories)
       (repo-section "Tools and Editors" "Developer tools, editor integrations, parser support, and repository workflow utilities." tool-repositories)
       (repo-section "Systems and Command Line" "Shell, core utilities, file systems, text processing, and low-level systems packages." systems-repositories)
       (repo-section "Network and Web" "HTTP, DNS, mail, SSH, web extraction, browser-facing, and transport projects." network-repositories)
       (repo-section "Data, Crypto, and Security" "Database, regex, crypto, secure messaging, analysis, and monitoring packages." data-security-repositories)
       (repo-section "Platforms and Integrations" "Mobile, cloud, GUI, media, ML, document, and third-party service integrations." platform-repositories)
+      (repo-section "Local and Archive Workspaces" "Adjacent Jerboa workspaces that are local-only or archival but still part of the current project inventory." local-repositories)
       (repo-section "Related Jerboa Ecosystem Projects" "Non-jerboa-prefixed projects that are part of the active Jerboa workbench." related-repositories)
       "</section>")))
 
@@ -257,13 +396,56 @@
       "</section>")))
 
 (before
-  (secure-headers! site-security-headers))
+  (let* ((req (current-request))
+         (path (if req (sinatra-request-path req) "/")))
+    (secure-headers! site-forced-security-headers)
+    (header! "Content-Security-Policy"
+             (if (repl-path? path)
+                 repl-content-security-policy
+                 normal-content-security-policy))))
 
 (GET "/assets/site.css"
   (content-type! "text/css; charset=utf-8")
   (cache-control! "public, max-age=3600")
   site-css)
 
+(GET "/favicon.ico"
+  (list 200
+        (append
+          (normal-headers "image/x-icon")
+          (list (cons "Cache-Control" "public, max-age=86400")))
+        favicon-ico))
+
+(GET "/assets/repl/repl.css"
+  (list 200 (repl-headers "text/css; charset=utf-8") repl-css))
+
+(GET "/assets/repl/v1/repl.css"
+  (list 200 (repl-headers "text/css; charset=utf-8") repl-css))
+
+(GET "/assets/repl/repl.js"
+  (list 200 (repl-headers "text/javascript; charset=utf-8") repl-js))
+
+(GET "/assets/repl/v1/repl.js"
+  (list 200 (repl-headers "text/javascript; charset=utf-8") repl-js))
+
+(GET "/assets/repl/repl-worker.js"
+  (list 200 (repl-headers "text/javascript; charset=utf-8") repl-worker-js))
+
+(GET "/assets/repl/v1/repl-worker.js"
+  (list 200 (repl-headers "text/javascript; charset=utf-8") repl-worker-js))
+
+(GET "/assets/repl/manifest.json"
+  (list 200 (repl-headers "application/json; charset=utf-8") repl-manifest-json))
+
+(GET "/assets/repl/v1/manifest.json"
+  (list 200 (repl-headers "application/json; charset=utf-8") repl-manifest-json))
+
+(GET "/assets/repl/jerboa-repl.wasm"
+  (list 200 (repl-headers "application/wasm") repl-wasm))
+
+(GET "/assets/repl/v1/jerboa-repl.wasm"
+  (list 200 (repl-headers "application/wasm") repl-wasm))
+
 (GET "/install.sh"
   (status! 503)
   (content-type! "text/plain; charset=utf-8")
@@ -290,6 +472,12 @@
   (content-type! "text/html; charset=utf-8")
   docs-page)
 
+(GET "/repl"
+  (redirect "/repl/" 301))
+
+(GET "/repl/"
+  (list 200 (repl-headers "text/html; charset=utf-8") repl-page))
+
 (GET "/packages"
   (content-type! "text/html; charset=utf-8")
   packages-page)
@@ -323,19 +511,73 @@
   (content-type! "text/html; charset=utf-8")
   (layout "Not Found" "" "<section class=\"content\"><h1>Not Found</h1><p class=\"lead\">That Jerboa page is not here.</p><p><a href=\"/\">Return home</a></p></section>"))
 
-(define (env-number name default)
+(define (fail . parts)
+  (let ((p (current-error-port)))
+    (for-each (lambda (x) (display x p)) parts)
+    (newline p))
+  (exit 1))
+
+(define (err-message e)
+  (guard (x (#t "unknown start failure"))
+    (condition-message e)))
+
+;; #t iff s is a non-empty string of ASCII digits, so "8443.5", "1+2i", "-1" and
+;; "#x1F" are all rejected before string->number ever sees them.
+(define (digit-string? s)
+  (let ((n (string-length s)))
+    (and (> n 0)
+         (let loop ((i 0))
+           (or (= i n)
+               (let ((c (string-ref s i)))
+                 (and (char>=? c #\0) (char<=? c #\9)
+                      (loop (+ i 1)))))))))
+
+;; PORT must be a positive integer in 1..65535; anything else exits with a clean
+;; error rather than being coerced by string->number (which would happily accept
+;; "8443.5" or "1+2i").
+(define (env-port name default)
   (let ((value (getenv name)))
-    (if value
-      (or (string->number value) default)
-      default)))
+    (if (not value)
+      default
+      (let ((port (and (digit-string? value) (string->number value))))
+        (if (and port (<= 1 port 65535))
+          port
+          (fail name " must be an integer between 1 and 65535 (got \"" value "\")"))))))
+
+;; Start HTTPSD and fail closed on any bind/start error. httpsd-start raises on a
+;; missing cert/key or a failed listen (e.g. port already in use) and returns a
+;; server on success; either way a failure must stop the process instead of
+;; looping forever on a server that never came up.
+(define (start-server port bind cert key)
+  (let ((server (guard (e (#t e))
+                  (httpsd-start port (sinatra-handler default-app) cert key
+                    'bind-address: bind))))
+    (cond
+      ((condition? server)
+       (fail "failed to start HTTPSD on " bind ":" port " — " (err-message server)))
+      ((not server)
+       (fail "failed to start HTTPSD on " bind ":" port " — no server returned"))
+      (else
+        (for-each display
+          (list "== Jerboa site listening on https://" bind ":" port " ==\n"))
+        server))))
 
 (define (main)
-  (let ((port (env-number "PORT" 8443))
-        (cert (getenv "TLS_CERT"))
-        (key (getenv "TLS_KEY")))
-    (run-https! default-app 'port: port 'cert: cert 'key: key)
-    (let loop ()
-      (thread-sleep! 3600)
-      (loop))))
+  (let* ((port (env-port "PORT" 8443))
+         (cert (getenv "TLS_CERT"))
+         (key (getenv "TLS_KEY"))
+         (insecure? (member "--insecure" (command-line-arguments)))
+         (tls? (and cert key)))
+    ;; Fail closed: never serve on a public interface without TLS. Without both
+    ;; TLS_CERT and TLS_KEY we refuse to start unless --insecure is given, and the
+    ;; bind address then defaults to loopback so plaintext is never exposed.
+    (when (and (not tls?) (not insecure?))
+      (fail "refusing to start: TLS_CERT and TLS_KEY must both be set to serve HTTPS "
+            "(pass --insecure to bind a plaintext listener to 127.0.0.1 only)"))
+    (let ((bind (or (getenv "BIND") (if tls? "0.0.0.0" "127.0.0.1"))))
+      (start-server port bind cert key)
+      (let loop ()
+        (thread-sleep! 3600)
+        (loop)))))
 
 (main)
diff --git a/tools/sync-jerboa b/tools/sync-jerboa
index a174f61..43848dc 100755
--- a/tools/sync-jerboa
+++ b/tools/sync-jerboa
@@ -67,10 +67,18 @@ trap 'rm -rf "$tmp_dir"' EXIT HUP INT TERM
 raw_snapshot=$tmp_dir/jerboa-source.raw.json
 snapshot=$tmp_dir/jerboa-source.json
 mv "$tmp_dir/jerboa-source.json" "$raw_snapshot"
+# Reflect the source tree's real dirty state rather than unconditionally claiming
+# clean: a dirty jerboa checkout must keep "working_tree_dirty": true so the
+# published snapshot never misrepresents a release built from uncommitted changes.
+if [ -z "$(git -C "$jerboa" status --porcelain 2>/dev/null)" ]; then
+  tree_dirty=false
+else
+  tree_dirty=true
+fi
 sed -E \
   -e 's#"path": "[^"]*/jerboa"#"path": "jerboa"#' \
   -e 's#"remote": "[^"]*git\.sr\.ht[:/]~lisp/jerboa"#"remote": "https://git.sr.ht/~lisp/jerboa"#' \
-  -e 's#"working_tree_dirty": true#"working_tree_dirty": false#' \
+  -e "s#\"working_tree_dirty\": (true|false)#\"working_tree_dirty\": $tree_dirty#" \
   "$raw_snapshot" > "$snapshot"
 target=data/jerboa-source.json