# MoCipher — Site specification

This document describes **mocipher.com** (Mohammed Rahhal's personal site): design language, layout, pages, scripts, APIs, security, privacy, and deployment. It is the single reference for aligning changes with intent.

**Design language:** *Field Station* — "paper by day, phosphor by night." Light mode is a **dossier** (warm paper, ink, amber stamps); dark mode is an **ops console** (deep space, phosphor teal, amber signals). Two voices: **Inter** (human) and a system **monospace** (machine). Theming is 100 % CSS-variable-driven — components never restate colours, so there are **no per-component dark-mode override blocks**.

---

## 1. Principles

| Principle | Implementation |
|-----------|----------------|
| **First-party front-end** | Public HTML loads scripts, styles, and fonts only from this origin (`/scripts/`, `/styles/`, `/assets/`). Libraries are **vendored** under `/assets/vendor/` (marked, DOMPurify, Prism, Quill). The only third-party browser resources are **Cloudflare Web Analytics** (privacy-first, cookieless beacon) and, on the contact form only, the **Cloudflare Turnstile** widget. |
| **Privacy-respecting** | No ad/engagement trackers; contact data stored first-party (KV); visitor IPs are **not** stored with submissions; optional Resend relay only when a server secret is set. Notice in `privacy.html` (CH/EU framing). **Credentials:** public certification titles come from the **KV timeline list** (admin **History** → type **Certification**) and drive the homepage grid, **Now** chips, and the dynamic `hasCredential` JSON-LD (`#person-credentials-ld`) built in `main.js` from `/api/timelines`. Optional per-item **`verifyUrl`** (https) opens issuer sites in a new tab. |
| **Secure defaults** | Strict CSP + related headers in `_headers`; admin routes `noindex`+`no-store`; PBKDF2 password hashing, optional **TOTP 2FA**, login rate-limiting, HttpOnly/Secure/SameSite session cookie; CSP violation reporting to `/api/csp-report`. |

**Server-side third parties (not browser scripts):** Cloudflare (Pages, Functions, KV, R2), optional **Resend** (email relay).

---

## 2. Design system

### 2.1 Typography (two voices)
- **Human voice — Inter** (variable), self-hosted `/assets/fonts/InterVariable.woff2`, declared in `/styles/fonts.css`. Body, headings, prose.
- **Machine voice — system monospace** (`--mono`: SF Mono / Menlo / Consolas …). Eyebrows, section stamps, meta, buttons, form labels, telemetry, code.
- Preload Inter in every page head.

### 2.2 Colour tokens (`styles/main.css`)
Same variable **names** as before; values re-skinned. Dark mode overrides the tokens only.

| Token | Light (dossier) | Dark (console) | Usage |
|-------|-----------------|----------------|-------|
| `--light` | `#f1eee6` | `#070c14` | Page |
| `--white` | `#faf8f2` | `#0d1522` | Card / surface |
| `--dark` | `#171f2b` | `#dbe6ee` | Text |
| `--dark-light` | `#3d4756` | `#a9b8c4` | Secondary text |
| `--gray` | `#6b7280` | `#7e8f9c` | Muted |
| `--gray-light` | `#d9d3c5` | `#1d2b3c` | Hairlines |
| `--primary` | `#0c7a6f` | `#2dd4bf` | Teal ink → phosphor |
| `--secondary` / `--accent` | `#b45309` | `#f5b944` | Amber stamp → signal |
| `--ring` / `--bracket` / `--panel-tint` / `--grid-dot` | teal/amber tints | phosphor tints | Focus ring, HUD brackets, panels, dot-grid |
| `--glow` | none (light) | phosphor glow | Dark-only emphasis |

Dark mode applies via `[data-theme="dark"]` **and** `@media (prefers-color-scheme: dark)` (unless `[data-theme="light"]`). **Theme-color:** `#f1eee6` (light) / `#070c14` (dark); `main.js` re-syncs the `<meta name="theme-color">` on manual toggle.

### 2.3 Layout
- `.container` — `max-width: 1160px`, padding `2rem`.
- `.static-document` — `max-width: 760px` (privacy, colophon).
- Body carries a subtle **dot-grid** background; nav is fixed, frosted (`color-mix` with solid fallback), mono links.

### 2.4 Signature elements
- **Section stamps** (`.sec-stamp`) — mono labels like `[ SEC.01 // IDENTITY ]`; the brackets are CSS-generated.
- **HUD corner brackets** — a shared `::after` on cards; tighten on hover.
- **Telemetry strip** — injected under the nav by `main.js`: live Zürich clock, `● CHANNEL SECURE`, current sector. Pauses while the tab is hidden.
- **Command palette (⌘K / Ctrl+K)** — injected by `main.js`: page navigation + actions (toggle theme, copy e-mail, download CV). Combobox a11y (`aria-activedescendant`).
- **Boot line** — one-per-session "establishing secure channel … [ ok ]" typed line in the hero.

### 2.5 Motion
- `scroll-behavior: smooth`; scroll-reveal via `[data-aos]` (opacity/transform only); hero typing animation.
- **`prefers-reduced-motion: reduce`** is honoured globally: animations/transitions disabled, typing hero shows the primary role statically, telemetry clock freezes, view-transitions off.

---

## 3. Pages

| File | Role (Field Station name) |
|------|---------------------------|
| `index.html` | Landing: hero (optional pinned-post strip), availability status band, about + skills, **History** (`#history`: highlights strip + collapsible full journey; non-certification items), **Certifications** (API), projects (+ `projects.html` link), latest posts, contact. Section stamps `SEC.01–06`. Person JSON-LD includes `@id`; `hasCredential` injected into `#person-credentials-ld`. Legacy **`#timeline`** redirects to **`#history`**. |
| `blog.html` | **Field Reports** — listing, search, tags, SPA-style post view, TOC, share, related posts, newsletter strip. |
| `cv.html` | **Personnel File** — Download / Open / embedded PDF, all pointing at **`/api/cv`**; iOS Safari gets a tap-to-open card (`main.js`). |
| `now.html` | **Status Board** — long-form snapshot; certification chips from `/api/timelines`. |
| `uses.html` | **Loadout** — stack / tools. |
| `projects.html` | **Operations index** — `/api/projects` with `#project-{id}` anchors. |
| `colophon.html` | **Build notes** — stack, fonts, analytics + security stance, links to spec + `llms.txt`. |
| `privacy.html` | CH/EU privacy notice (incl. analytics disclosure). |
| `llms.txt` | Plain-text context for assistants. |
| `admin.html` / `admin-login.html` | CMS (Quill, vendored). History/Certifications pipeline; **2FA** enrol in Settings; login prompts for a TOTP code when enrolled. |
| `404.html` | **SIGNAL LOST** — pure-CSS radar sweep + blip (own dark CSS). |
| `offline.html` | **AIR-GAPPED MODE** — cached-page links + retry (own dark CSS). |

**Open Graph / Twitter:** `og:image` + `twitter:image` = the designed **`/assets/images/og-card.png`** (1200×630) on all public pages; **JSON-LD `Person.image` keeps the real profile photo**. Card is regenerable from `tools/og-card.html`.

---

## 4. Front-end scripts

| Script | Responsibility |
|--------|----------------|
| `scripts/main.js` | Theme (toggle + theme-color meta sync), mobile nav (Escape closes, `aria-controls`), site-revision footers, homepage API hydration, History + certifications from `/api/timelines` (+ Now page + JSON-LD), social links (chained favicon paths, letter fallback), projects-only page, contact form POST (with Turnstile token when configured), PWA install, back-to-top, **command palette**, **telemetry strip**, **boot line**, **iOS CV fallback**, **Turnstile widget injection** (from `settings.turnstileSiteKey`). |
| `scripts/blog.js` | Fetch/filter/search posts, tag group (`role="group"`), open post, **lazy-load only the Prism language chunks a post uses** (dependency-resolved, incremental across navigation), related posts, OG/Twitter/canonical DOM updates, code copy buttons. |
| `scripts/admin.js` | Auth, CRUD (projects, posts w/ tags+ogImage, social, skills, timelines w/ `verifyUrl`/`highlight`), settings merge, **file uploads** (CV + images → `/api/upload`), **2FA** enrol/disable UI. |
| `scripts/now.js`, `scripts/404.js`, `scripts/offline.js` | Page-specific helpers. |

**Cache busting:** `?v=` on `main.js` / `blog.js` / `admin.js` / `admin-login.js`; bump on material change. Static CSS/JS are served minified in production (source stays readable — see §9).

---

## 5. Back-end (Cloudflare Pages Functions)

- **Bindings:** KV `MY_KVSITE`; R2 `FILES` (bucket `mocipher-files`); var `TURNSTILE_SITE_KEY` — all in `wrangler.toml`.
- **KV keys:** `settings`, `posts`, `projects`, `socialLinks`, `timeline`, `skills`, `contactSubmissions` (bounded, no IPs), `adminToken` (PBKDF2 hash), `sessionToken`, `totpSecret`/`totpPending`, plus `file:<uuid>` legacy blobs and ephemeral `rateLimit:*` / `loginAttempts:*`.
- **Uploads / files:** `POST /api/upload` (auth; PDF/image ≤10 MB) writes to **R2**; `GET /api/files/:key` reads **R2 first, then KV** (legacy). `GET /api/cv` 302-redirects to `settings.cvUrl` (validated http(s) or same-origin path) or the bundled fallback — no client JS, so CSP/cache can't leave it stale.
- **Routes:** `/api/posts`(+`/:id`), `/api/projects`(+`/:id`), `/api/social-links`(+`/:id`), `/api/timelines`(+`/:id`), `/api/skills`, `/api/settings`, `/api/contact`, `/api/cv`, `/api/upload`, `/api/files/:key`, `/api/export`, `/api/csp-report`, `/api/rss`, `/api/sitemap`; auth: `/api/auth/{login,logout,verify,setup,reset,2fa}`.
- **`/api/settings`** (public GET) strips deprecated keys and rewrites KV once; injects `turnstileSiteKey` from env. Persisted fields: hero/about/CV, `availability*`, `recruiter*`, `newsletter*`, `pinnedHomePostId`, `stat1–4Value/Label` — **merged** with the prior document on PUT.
- **`/api/export`** enumerates KV via `list()` (drift-proof), skipping ephemeral keys and credentials.
- **`functions/blog/[slug].js`** serves `blog.html` with per-post meta injected (`ogImage` → `image` → default).
- **Shared helpers:** `functions/api/_lib/security.js` (hashing, TOTP, sessions, rate-limit, `validateUrl`) and `_lib/http.js` (`json`/`badRequest`/`serverError`/`requireKV`/`readJson`).

---

## 6. Security

### 6.1 HTTP headers (`_headers`)
- HSTS (preload-ready), `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, restrictive `Permissions-Policy`.
- `X-Frame-Options: SAMEORIGIN` and CSP `frame-ancestors 'self'` (so the site can embed its own CV PDF).
- **CSP:** `default-src 'self'`; `script-src 'self' https://static.cloudflareinsights.com https://challenges.cloudflare.com`; `style-src 'self'`; `font-src 'self'`; `img-src 'self' data: https: blob:`; `connect-src 'self' https://cloudflareinsights.com https://challenges.cloudflare.com`; `frame-src 'self' https://challenges.cloudflare.com`; `object-src 'none'`; `base-uri 'self'`; `upgrade-insecure-requests`; `report-to csp-endpoint` (+ `Reporting-Endpoints` header → `/api/csp-report`).
- COOP/CORP `same-origin`; `/api/*` `noindex,nofollow`; `/scripts/*` + `/assets/*` immutable; admin HTML `no-store`.

### 6.2 Auth & abuse
- Password: PBKDF2-SHA256 (120k iterations), constant-time compare, legacy-hash upgrade on login.
- **2FA (TOTP, RFC 6238):** opt-in; login requires a code only once a secret is enrolled; the reset key clears it for recovery.
- Login/reset rate-limited per IP (5 / 15 min). Contact form: honeypot + custom-header CSRF + 5/hr rate limit + **Turnstile** (server-verified when `TURNSTILE_SECRET` is set).

### 6.3 Service worker (`sw.js`)
- Precaches the shell (pages, core CSS/JS, Inter, `profile-400`, marked+purify, manifest). **Prism chunks and Quill are excluded** — runtime-cached on first use.
- **Strategy:** network-first for `/api/*` **and** HTML navigations (fresh content, cache/offline fallback); cache-first for other static (`ignoreSearch` for `?v=`). Bump `CACHE_NAME` when the precache set changes.

### 6.4 Content safety
- Blog markdown → **marked** + **DOMPurify** before injection. Vendor versions (June 2026): DOMPurify 3.2.4, marked 15.0.1, Prism 1.29.0, Quill 2.0.3 — all current, no open CVEs.

---

## 7. Privacy (summary)
Full copy in `privacy.html`. In short: contact form stores name/email/message (no IP); bounded KV history; optional Resend when `RESEND_API_KEY` is set; **Cloudflare Web Analytics** (cookieless, no cross-site tracking); HttpOnly session cookie; SW cache for offline. Email domain is protected by SPF/DKIM/DMARC (see `docs/email-dns.md`).

---

## 8. Assets & vendor
- **Fonts:** `/assets/fonts/` — `InterVariable.woff2` only (Quicksand removed). `OFL.txt`/`README.txt` are Inter's licence.
- **Images:** profile (photo, used for JSON-LD + hero `profile-400.*`), **`og-card.png`** (social card), PWA icons **incl. dedicated maskable** (`icon-maskable-192/512`), `screenshot-wide/mobile` (Field Station), all `?v=` busted.
- **Icons:** cipher-hex monogram (hexagon + M + amber node = "M·O"); `favicon.ico`, `icon-192/512`, maskable, apple-touch — generated with ImageMagick MVG (see [[field-station-design]] / git history).
- **Vendor:** `/assets/vendor/` — marked, DOMPurify, Prism (+ language chunks), Quill (+ snow CSS). Runtime loading is first-party.

---

## 9. Deployment & tooling

- **Deploy is NOT wired to git.** `git push` does not deploy. Publish with **`npm run deploy`** (minifies CSS/JS via `tools/minify.mjs`, deploys, then restores readable source) or `npx wrangler pages deploy . --project-name=mocipher`.
- `wrangler.toml` binds KV + R2 + the Turnstile site-key var. **Secrets** (never committed): `TURNSTILE_SECRET`, optional `RESEND_API_KEY`, `ADMIN_RESET_KEY` — set via `wrangler pages secret put`.
- **Backup:** `workers/backup/` is a standalone Worker that snapshots KV → R2 (`mocipher-backups`). Its cron is disabled (account at the 5-trigger limit); it runs on demand.
- **Quality gate:** `npm run verify` = `check` (node --check all JS) + **`smoke`** (`tools/smoke.mjs`: every stylesheet parses, critical selectors present, each page keeps landmarks + valid asset refs) + **tests** (`node --test`, `test/*.mjs` — `security.js`, `_lib/http.js`, `/api/cv`). All three run in the **pre-commit hook**.
- **Checklist:** `_headers` deployed; KV+R2 bound; secrets set; bump `CACHE_NAME` after `sw.js` precache changes; update `privacy.html` date on process changes; set `SITE_CONTENT_REVISED_ISO` in `main.js` on substantive copy changes.

---

## 10. Related docs (repository)
- `README.md` — overview + setup.
- `docs/turnstile.md` — Turnstile key setup.
- `docs/email-dns.md` — SPF/DKIM/DMARC records.
- `QUICKSTART.md`, `PAGES-SETUP.md` — operational notes.
- `wrangler.toml` — bindings + deploy hints.

## 11. Accessibility (baseline)
- Skip link to `#main-content`; `aria-label` on icon-only controls; `aria-expanded`/`aria-controls` on the mobile nav toggle; Escape closes the menu and restores focus.
- Command palette uses combobox semantics; blog tag strip uses `aria-pressed` + `role="group"`.
- `:focus-visible` rings; **`prefers-reduced-motion`** respected globally (see §2.5).

---

*Document version: July 2026 — Field Station redesign: two-theme token system, ⌘K palette + telemetry, R2 uploads, 2FA, Turnstile, CSP reporting, Cloudflare Web Analytics, minify/smoke tooling, new icons + OG card.*
