and use the **dev login**
+(`ICEBERG_DEV_AUTH=true`, the default outside prod) — no IdP required.
+Install the [`typst`](https://github.com/typst/typst) binary for PDF
+rendering.
+
+!!! note "SQLite is dev/test only"
+ The app refuses to boot on a SQLite URL when `ICEBERG_ENVIRONMENT=prod`,
+ and the Docker image ships no SQLite fallback — a container can't
+ silently run without PostgreSQL.
+
+## Docker Compose (single host)
+
+```bash
+docker compose up # https://localhost via Caddy + PostgreSQL
+ICEBERG_DOMAIN=intel.example.com docker compose up # automatic Let's Encrypt TLS
+```
+
+The default Compose stack runs four services on an internal network:
+
+- **caddy** — the TLS front end and only public entry point (`:80`/`:443`,
+ HTTP/3). Automatic HTTPS: a local-CA certificate for the default
+ `localhost`, Let's Encrypt for a real domain. Runs non-root with all
+ capabilities dropped except `NET_BIND_SERVICE`, on a read-only rootfs.
+- **iceberg** — the app, publishing plain HTTP on loopback only
+ (`127.0.0.1:8000`) as a local/debug side door. Started with
+ `--proxy-headers`; `X-Forwarded-*` is trusted only from the Compose
+ network, so audit logs record the real client IP.
+- **postgres** — the datastore, on a named volume.
+- **redis** — shared rate-limit buckets across uvicorn workers.
+
+For production Compose, set `ICEBERG_ENVIRONMENT=prod` (Secure cookies +
+HSTS), a real `ICEBERG_SECRET_KEY`, `ICEBERG_AUTO_MIGRATE=false`, and
+non-default `POSTGRES_*` credentials.
+
+## Kubernetes
+
+Manifests live under
+[`deploy/k8s/`](https://github.com/IcebergAI/IcebergCTI/tree/main/deploy/k8s):
+an Ingress terminates TLS to a ClusterIP Service and a single-replica
+Deployment (`Recreate`, non-root, read-only rootfs, dropped capabilities). A
+migrate Job runs `alembic upgrade head` out of band; config comes from a
+ConfigMap and secrets from a Secret. PostgreSQL is a managed instance or the
+optional StatefulSet; uploads and rendered PDFs live on a `ReadWriteOnce`
+`/data` PVC — the reason replicas stay at 1 until shared storage lands.
+
+Pin the published image by immutable digest in production
+(`deploy/k8s/release.sh`).
+
+## Releases
+
+A release is a git tag: pushing `v*` to `main` builds and pushes
+`ghcr.io/icebergai/icebergcti` with an SBOM and SLSA provenance, attests the
+provenance to the registry, signs the image keylessly with cosign, and cuts
+the GitHub Release. See
+[`docs/RELEASING.md`](https://github.com/IcebergAI/IcebergCTI/blob/main/docs/RELEASING.md).
+
+## Backup and restore
+
+Persistent state lives in two places — **PostgreSQL** (reports, requirements,
+tags, audit events, settings) and the **`/data` volume** (attachments,
+figures, rendered PDFs). Back up both while writers are stopped:
+
+```bash
+docker compose stop iceberg
+docker compose exec postgres pg_dump -U iceberg -d iceberg -Fc > iceberg-$(date +%F).dump
+docker run --rm -v iceberg_iceberg-data:/data -v "$PWD":/out busybox \
+ tar cf /out/iceberg-data-$(date +%F).tar -C /data .
+docker compose start iceberg
+```
+
+Restore keeps the app stopped, restores PostgreSQL, clears the working
+directories before extracting the archive, applies migrations, and runs
+`iceberg-verify-files` before restart. PDFs regenerate; attachments and
+figures are original material.
+
+## Configuration
+
+All settings are environment variables with the `ICEBERG_` prefix (see
+[`.env.example`](https://github.com/IcebergAI/IcebergCTI/blob/main/.env.example)).
+Secrets are **env-only** — provider client secrets, API keys and tokens are
+never stored in the database. Admins get a read-only **effective
+configuration** view at `/admin/config` showing every resolved value, its
+provenance, and secret redaction, plus a settings hub at `/admin` with
+per-subsystem status tiles.
+
+Production guards refuse to boot on a SQLite URL, a weak
+`ICEBERG_SECRET_KEY`, or a wildcard `FORWARDED_ALLOW_IPS`.
diff --git a/website/docs/fonts/archivo-400-normal-latin-ext.woff2 b/website/docs/fonts/archivo-400-normal-latin-ext.woff2
new file mode 100644
index 0000000..2e544c2
Binary files /dev/null and b/website/docs/fonts/archivo-400-normal-latin-ext.woff2 differ
diff --git a/website/docs/fonts/archivo-400-normal-latin.woff2 b/website/docs/fonts/archivo-400-normal-latin.woff2
new file mode 100644
index 0000000..716d4ab
Binary files /dev/null and b/website/docs/fonts/archivo-400-normal-latin.woff2 differ
diff --git a/website/docs/fonts/archivo-500-normal-latin-ext.woff2 b/website/docs/fonts/archivo-500-normal-latin-ext.woff2
new file mode 100644
index 0000000..2e544c2
Binary files /dev/null and b/website/docs/fonts/archivo-500-normal-latin-ext.woff2 differ
diff --git a/website/docs/fonts/archivo-500-normal-latin.woff2 b/website/docs/fonts/archivo-500-normal-latin.woff2
new file mode 100644
index 0000000..716d4ab
Binary files /dev/null and b/website/docs/fonts/archivo-500-normal-latin.woff2 differ
diff --git a/website/docs/fonts/archivo-600-normal-latin-ext.woff2 b/website/docs/fonts/archivo-600-normal-latin-ext.woff2
new file mode 100644
index 0000000..2e544c2
Binary files /dev/null and b/website/docs/fonts/archivo-600-normal-latin-ext.woff2 differ
diff --git a/website/docs/fonts/archivo-600-normal-latin.woff2 b/website/docs/fonts/archivo-600-normal-latin.woff2
new file mode 100644
index 0000000..716d4ab
Binary files /dev/null and b/website/docs/fonts/archivo-600-normal-latin.woff2 differ
diff --git a/website/docs/fonts/archivo-700-normal-latin-ext.woff2 b/website/docs/fonts/archivo-700-normal-latin-ext.woff2
new file mode 100644
index 0000000..2e544c2
Binary files /dev/null and b/website/docs/fonts/archivo-700-normal-latin-ext.woff2 differ
diff --git a/website/docs/fonts/archivo-700-normal-latin.woff2 b/website/docs/fonts/archivo-700-normal-latin.woff2
new file mode 100644
index 0000000..716d4ab
Binary files /dev/null and b/website/docs/fonts/archivo-700-normal-latin.woff2 differ
diff --git a/website/docs/fonts/archivo-800-normal-latin-ext.woff2 b/website/docs/fonts/archivo-800-normal-latin-ext.woff2
new file mode 100644
index 0000000..2e544c2
Binary files /dev/null and b/website/docs/fonts/archivo-800-normal-latin-ext.woff2 differ
diff --git a/website/docs/fonts/archivo-800-normal-latin.woff2 b/website/docs/fonts/archivo-800-normal-latin.woff2
new file mode 100644
index 0000000..716d4ab
Binary files /dev/null and b/website/docs/fonts/archivo-800-normal-latin.woff2 differ
diff --git a/website/docs/fonts/jetbrains-mono-400-normal-latin-ext.woff2 b/website/docs/fonts/jetbrains-mono-400-normal-latin-ext.woff2
new file mode 100644
index 0000000..82f9668
Binary files /dev/null and b/website/docs/fonts/jetbrains-mono-400-normal-latin-ext.woff2 differ
diff --git a/website/docs/fonts/jetbrains-mono-400-normal-latin.woff2 b/website/docs/fonts/jetbrains-mono-400-normal-latin.woff2
new file mode 100644
index 0000000..4d09cda
Binary files /dev/null and b/website/docs/fonts/jetbrains-mono-400-normal-latin.woff2 differ
diff --git a/website/docs/fonts/jetbrains-mono-500-normal-latin-ext.woff2 b/website/docs/fonts/jetbrains-mono-500-normal-latin-ext.woff2
new file mode 100644
index 0000000..82f9668
Binary files /dev/null and b/website/docs/fonts/jetbrains-mono-500-normal-latin-ext.woff2 differ
diff --git a/website/docs/fonts/jetbrains-mono-500-normal-latin.woff2 b/website/docs/fonts/jetbrains-mono-500-normal-latin.woff2
new file mode 100644
index 0000000..4d09cda
Binary files /dev/null and b/website/docs/fonts/jetbrains-mono-500-normal-latin.woff2 differ
diff --git a/website/docs/fonts/jetbrains-mono-600-normal-latin-ext.woff2 b/website/docs/fonts/jetbrains-mono-600-normal-latin-ext.woff2
new file mode 100644
index 0000000..82f9668
Binary files /dev/null and b/website/docs/fonts/jetbrains-mono-600-normal-latin-ext.woff2 differ
diff --git a/website/docs/fonts/jetbrains-mono-600-normal-latin.woff2 b/website/docs/fonts/jetbrains-mono-600-normal-latin.woff2
new file mode 100644
index 0000000..4d09cda
Binary files /dev/null and b/website/docs/fonts/jetbrains-mono-600-normal-latin.woff2 differ
diff --git a/website/docs/fonts/jetbrains-mono-700-normal-latin-ext.woff2 b/website/docs/fonts/jetbrains-mono-700-normal-latin-ext.woff2
new file mode 100644
index 0000000..82f9668
Binary files /dev/null and b/website/docs/fonts/jetbrains-mono-700-normal-latin-ext.woff2 differ
diff --git a/website/docs/fonts/jetbrains-mono-700-normal-latin.woff2 b/website/docs/fonts/jetbrains-mono-700-normal-latin.woff2
new file mode 100644
index 0000000..4d09cda
Binary files /dev/null and b/website/docs/fonts/jetbrains-mono-700-normal-latin.woff2 differ
diff --git a/website/docs/fonts/spectral-400-italic-latin-ext.woff2 b/website/docs/fonts/spectral-400-italic-latin-ext.woff2
new file mode 100644
index 0000000..5295b22
Binary files /dev/null and b/website/docs/fonts/spectral-400-italic-latin-ext.woff2 differ
diff --git a/website/docs/fonts/spectral-400-italic-latin.woff2 b/website/docs/fonts/spectral-400-italic-latin.woff2
new file mode 100644
index 0000000..1e0f383
Binary files /dev/null and b/website/docs/fonts/spectral-400-italic-latin.woff2 differ
diff --git a/website/docs/fonts/spectral-400-normal-latin-ext.woff2 b/website/docs/fonts/spectral-400-normal-latin-ext.woff2
new file mode 100644
index 0000000..78c4c00
Binary files /dev/null and b/website/docs/fonts/spectral-400-normal-latin-ext.woff2 differ
diff --git a/website/docs/fonts/spectral-400-normal-latin.woff2 b/website/docs/fonts/spectral-400-normal-latin.woff2
new file mode 100644
index 0000000..71700f8
Binary files /dev/null and b/website/docs/fonts/spectral-400-normal-latin.woff2 differ
diff --git a/website/docs/fonts/spectral-500-normal-latin-ext.woff2 b/website/docs/fonts/spectral-500-normal-latin-ext.woff2
new file mode 100644
index 0000000..0dbd536
Binary files /dev/null and b/website/docs/fonts/spectral-500-normal-latin-ext.woff2 differ
diff --git a/website/docs/fonts/spectral-500-normal-latin.woff2 b/website/docs/fonts/spectral-500-normal-latin.woff2
new file mode 100644
index 0000000..440edbb
Binary files /dev/null and b/website/docs/fonts/spectral-500-normal-latin.woff2 differ
diff --git a/website/docs/fonts/spectral-600-normal-latin-ext.woff2 b/website/docs/fonts/spectral-600-normal-latin-ext.woff2
new file mode 100644
index 0000000..16fb98f
Binary files /dev/null and b/website/docs/fonts/spectral-600-normal-latin-ext.woff2 differ
diff --git a/website/docs/fonts/spectral-600-normal-latin.woff2 b/website/docs/fonts/spectral-600-normal-latin.woff2
new file mode 100644
index 0000000..3f56161
Binary files /dev/null and b/website/docs/fonts/spectral-600-normal-latin.woff2 differ
diff --git a/website/docs/index.md b/website/docs/index.md
new file mode 100644
index 0000000..7839a73
--- /dev/null
+++ b/website/docs/index.md
@@ -0,0 +1,113 @@
+---
+title: Iceberg
+hide:
+ - navigation
+ - toc
+---
+
+
+
+

+
+
Cyber threat intelligence
+
+Collect. Author. Disseminate.
+
+
+Iceberg is a CTI platform built around the intelligence cycle: analysts collect
+sources into topic **notebooks**, author finished **intelligence products** in
+markdown, and **disseminate** them to stakeholders matched by intelligence level
+and TLP — with the whole loop closed by stakeholder feedback.
+
+
+
+[The intelligence cycle](workflow.md){ .md-button .md-button--primary }
+[Deploy it](deployment.md){ .md-button }
+[View on GitHub](https://github.com/IcebergAI/IcebergCTI){ .md-button }
+
+
+
+
+## Why Iceberg
+
+Most CTI tooling is an indicator database with a report feature bolted on.
+Iceberg inverts that: it is a **finished-intelligence platform** — the
+authoritative IOC store stays external (MISP) — built for the analysts who
+write, the reviewers who gate, and the stakeholders who consume. Reports carry
+an intelligence level (Strategic / Tactical / Operational), a TLP marking,
+Admiralty-graded sources, and structured analytic techniques, and they move
+through a review workflow to a frozen, immutable publication.
+
+
+
+
+
+- :material-notebook-outline: __Notebook-based collection__
+
+ ---
+
+ Open a topic notebook, gather sources, notes, and uploaded attachments;
+ stage light-touch IOCs; apply the Diamond Model and ACH with live inline
+ embeds.
+
+ [:octicons-arrow-right-24: Collection](workflow.md#collect)
+
+- :material-file-document-edit-outline: __Author finished products__
+
+ ---
+
+ A full-height markdown editor with live preview, source citations,
+ requirement traceability, taxonomy tagging, and branded Typst PDF
+ rendering.
+
+ [:octicons-arrow-right-24: Authoring](workflow.md#author)
+
+- :material-send-check-outline: __Dissemination that closes the loop__
+
+ ---
+
+ On publish, reports match stakeholders by preferred intel level + TLP into
+ a personal feed with email notification; feedback and RFI satisfaction
+ flow back to analysts.
+
+ [:octicons-arrow-right-24: Dissemination](workflow.md#disseminate)
+
+- :material-clipboard-list-outline: __Requirements → tasking__
+
+ ---
+
+ Stakeholders record PIRs, GIRs and RFIs; analysts work them from a
+ tasking board with coverage gaps and traceability from requirement to
+ published product.
+
+ [:octicons-arrow-right-24: Tasking](workflow.md#task)
+
+- :material-shield-lock-outline: __Governed by design__
+
+ ---
+
+ Multi-provider OIDC SSO, role-based access, TLP-gated egress for AI, MISP
+ and dissemination, a structured audit log with SIEM forwarding, and
+ hardened outbound fetching.
+
+ [:octicons-arrow-right-24: Security](security.md)
+
+- :material-connection: __Plays well with your stack__
+
+ ---
+
+ MISP push, read-only TAXII 2.1 serving, TAXII/MISP pull, RSS ingestion,
+ ATT&CK Navigator layers, governed AI assist, publication webhooks, and a
+ global outbound proxy.
+
+ [:octicons-arrow-right-24: Integrations](integrations.md)
+
+
+
+## The product, not the pipeline
+
+Everything converges on the finished report: TLP and intelligence-level
+markings, numbered citations, an indicators appendix, inline Diamond/ACH/figure
+embeds, and one-click PDF products typeset with Typst.
+
+
diff --git a/website/docs/integrations.md b/website/docs/integrations.md
new file mode 100644
index 0000000..0a6aab3
--- /dev/null
+++ b/website/docs/integrations.md
@@ -0,0 +1,76 @@
+---
+title: Integrations
+icon: material/connection
+---
+
+# Integrations
+
+Iceberg is API-first and designed to sit inside an existing security stack.
+Every outbound HTTP call honours the **global outbound proxy** configured at
+`/admin/proxy` (system / direct / explicit with no-proxy exclusions).
+Publication-driven delivery — email notifications, publication webhooks — and
+RSS polling ride a durable **outbox job queue** drained by the
+`iceberg-worker` process, enqueued in the same transaction as their cause,
+with lease, retry and backoff; interactive calls (the MISP push, AI assist,
+writer-triggered TAXII/MISP pulls) run synchronously and report their result
+directly.
+
+## MISP (outbound push)
+
+Iceberg stages **light-touch IOCs** as notebook entities; the authoritative
+indicator store stays external. A writer pushes a report's **cited
+indicators to MISP as one event** — lifecycle-gated (approved or published
+reports only; published reports push from their frozen snapshot), idempotent
+(re-push updates the same event), failure-isolated, and authenticated with an
+env-only API key. Each indicator's TLP rides along as a per-attribute tag;
+indicators above `ICEBERG_MISP_MAX_TLP` prompt the writer to confirm before
+leaving the org. Configured at `/admin/misp`.
+
+## TAXII 2.1 (serving and pull)
+
+- **Serving** — published reports are available read-only in TAXII
+ 2.1-shaped collections under `/api/taxii2/`, as STIX 2.1 `report` objects
+ with their cited indicators.
+- **Pull** — writers trigger a TAXII 2.1 (or MISP) pull into a notebook,
+ landing objects as sources and staged IOCs behind the SSRF guard.
+
+## RSS / Atom ingestion
+
+Admins configure external feeds at `/admin/feeds`; articles poll into a
+writer-only **feed reader** where an analyst sends an article to a notebook
+as an auto-graded source. Fetching is opt-in, timeout- and byte-bounded,
+failure-isolated, and content is sanitised; feed URLs are admin-only — the
+SSRF containment boundary.
+
+## ATT&CK
+
+An offline **MITRE Enterprise ATT&CK** bundle import
+(`iceberg-import-attack`) creates technique tags. Techniques tagged across
+reports drive a coverage heatmap and downloadable **ATT&CK Navigator
+layers** — per report and per actor/malware/campaign entity.
+
+## Governed AI assist
+
+Optional AI-assist endpoints (summarise a source, suggest IOC candidates,
+draft tradecraft phrasing) run behind an admin-configured provider
+(`/admin/ai`) with an env-only key. Source content above
+`ICEBERG_AI_MAX_TLP` never leaves for the provider; AI-suggested indicators
+are normalised (refanged, type-constrained) and always land as *staged*
+entities for analyst review, never directly in a product.
+
+## Email, webhooks and SIEM
+
+- **Email** — stakeholders get a notification when a product lands in their
+ feed (SMTP, optional).
+- **Publication webhook** — a signed webhook fires on publish for
+ tag-subscription integrations, configured at `/admin/webhook` with an
+ env-only token.
+- **SIEM** — the structured audit log forwards via stdout/file, syslog, or
+ HTTP event collector (see [Security](security.md#audit-and-siem)).
+
+## API
+
+The whole portal is backed by a JSON API under `/api/*`, authenticated with
+Bearer JWTs — everything an analyst can do in the UI, a pipeline can do
+against the API. Interactive docs (`/docs`, `/redoc`) are enabled outside
+production.
diff --git a/website/docs/security.md b/website/docs/security.md
new file mode 100644
index 0000000..6ef5a11
--- /dev/null
+++ b/website/docs/security.md
@@ -0,0 +1,88 @@
+---
+title: Security
+icon: material/shield-lock-outline
+---
+
+# Security
+
+Iceberg is built for security-conscious environments: SSO-only authentication,
+least-privilege roles, marking-gated egress, and an audit trail designed for a
+SIEM.
+
+## Authentication
+
+Authentication is **multi-provider OIDC** — Microsoft Entra, Authentik,
+Auth0 and Okta, simultaneously — admin-configured at `/admin/oidc` (env
+seeds it). After login Iceberg mints its own short-lived **JWT**, sent as a
+Bearer header by API clients or carried in a session cookie by the portal; a
+per-user `token_version` supports logout and revocation, and the JWT and
+session cookie are signed with **purpose-separated keys** derived from
+`ICEBERG_SECRET_KEY`.
+
+Users are keyed on the immutable `(auth_provider, issuer, sub)` triple —
+email is non-identifying, so the same person under two IdPs is two accounts,
+and a spoofed issuer can't inherit another provider's users. A per-provider
+`role_map` maps IdP groups to roles, defaulting to least-privilege
+STAKEHOLDER; a missing or unverified email is rejected. Per-provider client
+secrets are **env-only**.
+
+A **dev-login bypass** (`ICEBERG_DEV_AUTH=true`) issues a role JWT without an
+IdP for local development; it is disabled in prod.
+
+## Roles
+
+| Role | Capability |
+|---|---|
+| `ADMIN` | Everything, plus admin configuration surfaces |
+| `ANALYST` | Collect, author, publish, push to MISP |
+| `REVIEWER` | As analyst, plus the review gate |
+| `STAKEHOLDER` | Read-only: feed, published reports, own requirements, feedback |
+
+Notebook access is **role-wide** for writers, not owner-scoped — `owner_id`
+is provenance only; the one owner/admin gate is deleting a whole notebook.
+
+## TLP as an egress gate
+
+TLP is a display and dissemination-routing marking — never an in-portal read
+gate. Where it *does* gate is at the boundary: content above the configured
+ceiling cannot leave through **AI assist** (`ICEBERG_AI_MAX_TLP`),
+**dissemination** (`ICEBERG_DISSEMINATION_MAX_TLP`), or the **MISP push**
+(`ICEBERG_MISP_MAX_TLP`).
+
+## Hardening
+
+- **CSRF** — a same-origin middleware rejects cookie-authenticated
+ state-changing requests whose `Origin`/`Referer` mismatch the host;
+ Bearer and anonymous requests are exempt; logout is POST-only.
+- **Strict CSP** — `script-src 'self'` with no `unsafe-inline`/`unsafe-eval`:
+ a vendored CSP build of Alpine, SRI-pinned self-hosted assets, no CDN
+ contact at runtime.
+- **SSRF guard** — the one server-side fetcher (RSS and writer-triggered
+ TAXII/MISP pull) is http(s)-only, rejects private/loopback addresses,
+ bounds bytes and timeouts, and re-validates every redirect hop.
+- **Upload validation** — MIME whitelist, size caps, and magic-byte content
+ sniffing.
+- **Rate limiting** and an oversized-body rejection before anything buffers;
+ security response headers set by the app itself.
+- **Immutable publications** — stakeholder, PDF and MISP output serve from a
+ frozen publication snapshot; an optimistic-lock version column turns a
+ stale write into a 409.
+- **Prod guards** — boot refuses SQLite, a weak secret key, or wildcard
+ proxy trust in production.
+
+## Audit and SIEM
+
+Security-relevant events are captured to a **structured-JSON audit log**
+(OWASP application-logging shape) with the real client IP, and can be
+forwarded to a SIEM via stdout/file, syslog, or an HTTP event collector —
+configured at `/admin/audit`. The audit detail JSON never carries secrets or
+PII payloads.
+
+## Supply chain
+
+CI runs ruff, bandit, vulture, mypy, pip-audit, CodeQL SAST, a Trivy image
+scan, and a zizmor + actionlint audit of the workflows themselves; all
+GitHub Actions are SHA-pinned. Release images ship an SBOM and SLSA
+provenance and are cosign-signed. See
+[`SECURITY.md`](https://github.com/IcebergAI/IcebergCTI/blob/main/SECURITY.md)
+for the vulnerability disclosure policy.
diff --git a/website/docs/stylesheets/fonts.css b/website/docs/stylesheets/fonts.css
new file mode 100644
index 0000000..879119f
--- /dev/null
+++ b/website/docs/stylesheets/fonts.css
@@ -0,0 +1,275 @@
+/* Docs-site copy of the app's font sheet — derived from static/css/vendor/fonts.css
+ with the /static/fonts/ URLs rewritten to ../fonts/ (Zensical serves the woff2
+ files from website/docs/fonts/). The app sheet is the source of truth;
+ font = false in zensical.toml disables Google Fonts so these are the only
+ faces loaded. */
+
+/* Archivo 400 normal latin-ext */
+@font-face {
+ font-family: 'Archivo';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(../fonts/archivo-400-normal-latin-ext.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* Archivo 400 normal latin */
+@font-face {
+ font-family: 'Archivo';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(../fonts/archivo-400-normal-latin.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* Archivo 500 normal latin-ext */
+@font-face {
+ font-family: 'Archivo';
+ font-style: normal;
+ font-weight: 500;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(../fonts/archivo-500-normal-latin-ext.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* Archivo 500 normal latin */
+@font-face {
+ font-family: 'Archivo';
+ font-style: normal;
+ font-weight: 500;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(../fonts/archivo-500-normal-latin.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* Archivo 600 normal latin-ext */
+@font-face {
+ font-family: 'Archivo';
+ font-style: normal;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(../fonts/archivo-600-normal-latin-ext.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* Archivo 600 normal latin */
+@font-face {
+ font-family: 'Archivo';
+ font-style: normal;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(../fonts/archivo-600-normal-latin.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* Archivo 700 normal latin-ext */
+@font-face {
+ font-family: 'Archivo';
+ font-style: normal;
+ font-weight: 700;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(../fonts/archivo-700-normal-latin-ext.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* Archivo 700 normal latin */
+@font-face {
+ font-family: 'Archivo';
+ font-style: normal;
+ font-weight: 700;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(../fonts/archivo-700-normal-latin.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* Archivo 800 normal latin-ext */
+@font-face {
+ font-family: 'Archivo';
+ font-style: normal;
+ font-weight: 800;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(../fonts/archivo-800-normal-latin-ext.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* Archivo 800 normal latin */
+@font-face {
+ font-family: 'Archivo';
+ font-style: normal;
+ font-weight: 800;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(../fonts/archivo-800-normal-latin.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* JetBrains Mono 400 normal latin-ext */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(../fonts/jetbrains-mono-400-normal-latin-ext.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* JetBrains Mono 400 normal latin */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(../fonts/jetbrains-mono-400-normal-latin.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* JetBrains Mono 500 normal latin-ext */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(../fonts/jetbrains-mono-500-normal-latin-ext.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* JetBrains Mono 500 normal latin */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(../fonts/jetbrains-mono-500-normal-latin.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* JetBrains Mono 600 normal latin-ext */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(../fonts/jetbrains-mono-600-normal-latin-ext.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* JetBrains Mono 600 normal latin */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(../fonts/jetbrains-mono-600-normal-latin.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* JetBrains Mono 700 normal latin-ext */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(../fonts/jetbrains-mono-700-normal-latin-ext.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* JetBrains Mono 700 normal latin */
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(../fonts/jetbrains-mono-700-normal-latin.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* Spectral 400 italic latin-ext */
+@font-face {
+ font-family: 'Spectral';
+ font-style: italic;
+ font-weight: 400;
+ font-display: swap;
+ src: url(../fonts/spectral-400-italic-latin-ext.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* Spectral 400 italic latin */
+@font-face {
+ font-family: 'Spectral';
+ font-style: italic;
+ font-weight: 400;
+ font-display: swap;
+ src: url(../fonts/spectral-400-italic-latin.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* Spectral 400 normal latin-ext */
+@font-face {
+ font-family: 'Spectral';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(../fonts/spectral-400-normal-latin-ext.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* Spectral 400 normal latin */
+@font-face {
+ font-family: 'Spectral';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(../fonts/spectral-400-normal-latin.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* Spectral 500 normal latin-ext */
+@font-face {
+ font-family: 'Spectral';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(../fonts/spectral-500-normal-latin-ext.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* Spectral 500 normal latin */
+@font-face {
+ font-family: 'Spectral';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(../fonts/spectral-500-normal-latin.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+/* Spectral 600 normal latin-ext */
+@font-face {
+ font-family: 'Spectral';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(../fonts/spectral-600-normal-latin-ext.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* Spectral 600 normal latin */
+@font-face {
+ font-family: 'Spectral';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url(../fonts/spectral-600-normal-latin.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
diff --git a/website/docs/stylesheets/iceberg.css b/website/docs/stylesheets/iceberg.css
new file mode 100644
index 0000000..9b6eec7
--- /dev/null
+++ b/website/docs/stylesheets/iceberg.css
@@ -0,0 +1,306 @@
+/* ==========================================================================
+ * IcebergCTI docs — Iceberg design system mapped onto the Material theme.
+ *
+ * Strategy: define the Iceberg oklch tokens per colour scheme, then point
+ * Material's --md-* variables at them so the whole theme recolours. The
+ * header is the app's dark command-center "rail" in BOTH schemes; links and
+ * interactive accents are the fixed glacial-cyan.
+ *
+ * Token source of truth: src/iceberg/static/css/iceberg.css. Kept in step with
+ * the sibling IcebergTTX/EBS docs sheets so the family sites read as one system.
+ * ========================================================================== */
+
+/* ---- Fonts (self-hosted; @font-face rules live in fonts.css) ------------- */
+:root {
+ --md-text-font: "Archivo";
+ --md-code-font: "JetBrains Mono";
+}
+
+/* ==========================================================================
+ * LIGHT scheme
+ * ========================================================================== */
+[data-md-color-scheme="default"] {
+ /* Iceberg tokens (light) */
+ --paper: oklch(0.984 0.006 240);
+ --surface: oklch(1 0 0);
+ --surface-2: oklch(0.972 0.008 240);
+ --surface-ink: oklch(0.22 0.026 256);
+ --ink: oklch(0.26 0.026 262);
+ --ink-soft: oklch(0.44 0.022 260);
+ --muted: oklch(0.58 0.017 258);
+ --faint: oklch(0.70 0.012 258);
+ --line: oklch(0.915 0.008 248);
+ --line-strong: oklch(0.855 0.012 248);
+
+ --accent: oklch(0.66 0.118 226);
+ --accent-ink: oklch(0.50 0.115 234);
+ --accent-deep: oklch(0.42 0.10 238);
+ --accent-soft: oklch(0.955 0.028 226);
+ --accent-line: oklch(0.86 0.055 226);
+ --accent-glow: oklch(0.66 0.118 226 / 0.16);
+
+ --rail: oklch(0.225 0.024 256);
+ --rail-2: oklch(0.285 0.026 256);
+ --rail-line: oklch(0.34 0.022 256);
+ --rail-ink: oklch(0.80 0.02 250);
+
+ /* Material mapping */
+ --md-default-bg-color: var(--paper);
+ --md-default-bg-color--light: color-mix(in oklch, var(--paper), transparent 30%);
+ --md-default-fg-color: var(--ink);
+ --md-default-fg-color--light: var(--ink-soft);
+ --md-default-fg-color--lighter: var(--muted);
+ --md-default-fg-color--lightest: var(--line);
+
+ --md-primary-fg-color: var(--rail); /* header background */
+ --md-primary-fg-color--light: var(--rail-2);
+ --md-primary-fg-color--dark: var(--surface-ink);
+ --md-primary-bg-color: oklch(0.97 0.01 250);
+ --md-primary-bg-color--light: var(--rail-ink);
+
+ --md-accent-fg-color: var(--accent-ink);
+ --md-accent-fg-color--transparent: var(--accent-glow);
+
+ --md-typeset-a-color: var(--accent-ink);
+ --md-typeset-mark-color: var(--accent-glow);
+
+ --md-code-bg-color: var(--surface-2);
+ --md-code-fg-color: var(--ink);
+
+ --md-footer-bg-color: var(--surface-ink);
+ --md-footer-bg-color--dark: oklch(0.15 0.02 256);
+ --md-footer-fg-color: oklch(0.82 0.02 250);
+ --md-footer-fg-color--light: oklch(0.62 0.018 252);
+ --md-footer-fg-color--lighter: oklch(0.50 0.016 254);
+}
+
+/* ==========================================================================
+ * DARK (slate) scheme
+ * ========================================================================== */
+[data-md-color-scheme="slate"] {
+ --paper: oklch(0.185 0.02 256);
+ --surface: oklch(0.23 0.022 256);
+ --surface-2: oklch(0.27 0.024 256);
+ --surface-ink: oklch(0.15 0.02 256);
+ --ink: oklch(0.93 0.012 250);
+ --ink-soft: oklch(0.80 0.015 252);
+ --muted: oklch(0.66 0.016 255);
+ --faint: oklch(0.55 0.014 256);
+ --line: oklch(0.32 0.018 256);
+ --line-strong: oklch(0.40 0.022 256);
+
+ --accent: oklch(0.72 0.12 226);
+ --accent-ink: oklch(0.80 0.11 226);
+ --accent-deep: oklch(0.86 0.085 226);
+ --accent-soft: oklch(0.30 0.05 232);
+ --accent-line: oklch(0.44 0.07 230);
+ --accent-glow: oklch(0.72 0.12 226 / 0.22);
+
+ --rail: oklch(0.16 0.018 256);
+ --rail-2: oklch(0.235 0.022 256);
+ --rail-line: oklch(0.30 0.02 256);
+ --rail-ink: oklch(0.82 0.02 250);
+
+ --md-default-bg-color: var(--paper);
+ --md-default-bg-color--light: color-mix(in oklch, var(--paper), transparent 30%);
+ --md-default-fg-color: var(--ink);
+ --md-default-fg-color--light: var(--ink-soft);
+ --md-default-fg-color--lighter: var(--muted);
+ --md-default-fg-color--lightest: var(--line);
+
+ --md-primary-fg-color: var(--rail);
+ --md-primary-fg-color--light: var(--rail-2);
+ --md-primary-fg-color--dark: var(--surface-ink);
+ --md-primary-bg-color: var(--ink);
+ --md-primary-bg-color--light: var(--rail-ink);
+
+ --md-accent-fg-color: var(--accent-ink);
+ --md-accent-fg-color--transparent: var(--accent-glow);
+
+ --md-typeset-a-color: var(--accent-ink);
+ --md-typeset-mark-color: var(--accent-glow);
+
+ --md-code-bg-color: var(--surface-ink);
+ --md-code-fg-color: var(--ink);
+
+ --md-footer-bg-color: var(--surface-ink);
+ --md-footer-bg-color--dark: oklch(0.12 0.018 256);
+ --md-footer-fg-color: var(--rail-ink);
+ --md-footer-fg-color--light: var(--muted);
+ --md-footer-fg-color--lighter: var(--faint);
+}
+
+/* ==========================================================================
+ * Chrome
+ * ========================================================================== */
+
+/* 3px glacial-cyan brandbar strip pinned above the (dark) header */
+.md-header {
+ border-top: 3px solid var(--accent);
+ box-shadow: 0 1px 0 var(--rail-line);
+}
+.md-tabs {
+ background-color: var(--md-primary-fg-color--dark);
+}
+
+/* Search field: darker well on the dark header */
+.md-search__form {
+ background-color: color-mix(in oklch, var(--rail-2), transparent 10%);
+}
+.md-search__form:hover {
+ background-color: var(--rail-2);
+}
+
+/* Links: cyan, with an accent underline on hover */
+.md-typeset a {
+ text-decoration-color: var(--accent-line);
+}
+.md-typeset a:hover {
+ color: var(--accent);
+}
+
+/* ==========================================================================
+ * Typography — Spectral (serif) for the editorial layer
+ * ========================================================================== */
+.md-typeset h1 {
+ font-family: "Spectral", Georgia, serif;
+ font-weight: 600;
+ letter-spacing: -0.01em;
+ color: var(--ink);
+}
+.md-typeset blockquote {
+ font-family: "Spectral", Georgia, serif;
+ border-left: 3px solid var(--accent);
+ color: var(--ink-soft);
+}
+
+/* Mono eyebrow / small-caps kicker used on pages */
+.md-typeset .eyebrow {
+ font-family: "JetBrains Mono", monospace;
+ font-size: 0.72rem;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--accent-ink);
+}
+
+/* Inline code + code blocks pick up token surfaces */
+.md-typeset code {
+ border: 1px solid var(--line);
+ border-radius: 6px;
+}
+
+/* ==========================================================================
+ * Hero (landing page) — dark command-center panel
+ * ========================================================================== */
+.iceberg-hero {
+ margin: -1rem 0 2.5rem;
+ padding: 3rem 2.5rem 3.25rem;
+ border-radius: 14px;
+ border: 1px solid var(--rail-line);
+ background:
+ radial-gradient(120% 140% at 100% 0%, oklch(0.34 0.06 232 / 0.55), transparent 55%),
+ linear-gradient(160deg, var(--rail-2), var(--surface-ink));
+ color: oklch(0.93 0.012 250);
+ box-shadow: 0 20px 48px -24px oklch(0.15 0.03 256 / 0.7);
+}
+.iceberg-hero img.hero-wordmark {
+ height: 3.1rem;
+ width: auto;
+ margin: 0 0 1.25rem;
+}
+.iceberg-hero .eyebrow {
+ color: var(--accent-deep);
+ margin-bottom: 0.75rem;
+}
+.iceberg-hero p.tagline {
+ font-family: "Spectral", Georgia, serif;
+ font-size: 1.28rem;
+ line-height: 1.5;
+ max-width: 44ch;
+ color: oklch(0.86 0.015 252);
+ margin: 0.25rem 0 1.75rem;
+}
+.iceberg-hero .hero-cta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+}
+
+/* Primary/ghost buttons (Material .md-button) tuned to the cyan accent */
+.md-typeset .md-button {
+ border-radius: 8px;
+ border-color: var(--accent-line);
+ color: var(--accent-deep);
+ transition: background-color .15s, color .15s, border-color .15s;
+}
+.md-typeset .md-button:focus,
+.md-typeset .md-button:hover {
+ background-color: var(--accent);
+ border-color: var(--accent);
+ color: oklch(0.16 0.02 256);
+}
+.md-typeset .md-button--primary {
+ background-color: var(--accent);
+ border-color: var(--accent);
+ color: oklch(0.16 0.02 256);
+}
+.md-typeset .md-button--primary:hover {
+ background-color: var(--accent-ink);
+ border-color: var(--accent-ink);
+ color: oklch(0.98 0.01 250);
+}
+/* The hero panel is dark in BOTH schemes, so its buttons use fixed colours.
+ * The theme's plain .md-button carries a --line background, which is
+ * near-white in the light scheme — the light hero text would vanish on it. */
+.iceberg-hero .md-button {
+ background-color: oklch(1 0 0 / 0.08);
+ border-color: color-mix(in oklch, var(--accent), transparent 40%);
+ color: oklch(0.9 0.02 232);
+}
+.iceberg-hero .md-button--primary {
+ background-color: oklch(0.66 0.118 226);
+ border-color: oklch(0.66 0.118 226);
+ color: oklch(0.16 0.02 256);
+}
+.iceberg-hero .md-button--primary:focus,
+.iceberg-hero .md-button--primary:hover {
+ background-color: oklch(0.78 0.10 226);
+ border-color: oklch(0.78 0.10 226);
+ color: oklch(0.16 0.02 256);
+}
+
+/* ==========================================================================
+ * Grid cards — surface panels with an accent hover
+ * ========================================================================== */
+.md-typeset .grid.cards > ul > li,
+.md-typeset .grid > .card {
+ border: 1px solid var(--line);
+ border-radius: 12px;
+ background-color: var(--surface);
+ box-shadow: 0 1px 2px oklch(0.45 0.03 256 / 0.05);
+ transition: border-color .15s, box-shadow .15s, transform .15s;
+}
+.md-typeset .grid.cards > ul > li:hover,
+.md-typeset .grid > .card:hover {
+ border-color: var(--accent-line);
+ box-shadow: 0 10px 26px -14px oklch(0.4 0.04 256 / 0.35);
+ transform: translateY(-2px);
+}
+.md-typeset .grid.cards > ul > li > hr {
+ border-bottom-color: var(--line);
+}
+
+/* Screenshots: framed with the token line + soft lift */
+.md-typeset img.shot {
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ box-shadow: 0 12px 32px -18px oklch(0.4 0.04 256 / 0.4);
+}
+
+/* Admonitions: accent the "note/info" flavour to cyan */
+.md-typeset .admonition.note,
+.md-typeset .admonition.info,
+.md-typeset details.note,
+.md-typeset details.info {
+ border-left-color: var(--accent);
+}
diff --git a/website/docs/workflow.md b/website/docs/workflow.md
new file mode 100644
index 0000000..fcaa4cb
--- /dev/null
+++ b/website/docs/workflow.md
@@ -0,0 +1,114 @@
+---
+title: The intelligence cycle
+icon: material/refresh
+---
+
+# The intelligence cycle
+
+Iceberg follows the traditional intelligence cycle — direction, collection,
+analysis, production, dissemination, feedback — with a screen for each phase
+and traceability across all of them.
+
+## Direction: requirements and tasking { #task }
+
+Stakeholders (read-only users) record their own intelligence requirements,
+typed by how they direct work:
+
+| Type | Meaning | Shape |
+|---|---|---|
+| **PIR** | Priority intelligence requirement | Decision-tied, time-bound, carries priority |
+| **GIR** | General intelligence requirement | Standing collection interest |
+| **RFI** | Request for information | Ad-hoc question, satisfied by a specific product |
+
+Requirements aggregate into a **tasking board** grouped by status. Ordering
+blends urgency and kind — a PIR is floored to at least High priority so it
+leads standing work, but a genuine Critical item still tops its column. A PIR
+coverage panel flags PIRs with no linked report or notebook (collection gaps)
+and those past their review-by date.
+
+
+
+## Collection: notebooks { #collect }
+
+Analysts open a topic **notebook** and gather material into it:
+
+- **Sources** — links and references, graded with an offline Admiralty/NATO
+ reliability heuristic; manual sources default to TLP:AMBER.
+- **Notes** — free-form analyst notes.
+- **Attachments** — uploaded reference files (MIME-whitelisted, size-capped,
+ magic-byte validated).
+- **Light-touch IOCs** — indicators staged as notebook entities for later
+ citation and MISP push. Iceberg is *not* an IOC store; the authoritative
+ store stays external.
+
+Collection is fed from outside too: an admin-configured **RSS/Atom reader**
+lets analysts send an article to a notebook as an auto-graded source, and
+writers can pull from **TAXII 2.1** and **MISP** servers directly into
+notebook sources and IOCs.
+
+
+
+## Analysis: structured techniques
+
+Inside a notebook, analysts apply structured analytic techniques whose
+artefacts embed live into reports:
+
+- **Diamond Model** of intrusion analysis diagrams
+- **Analysis of Competing Hypotheses (ACH)** matrices
+- **Figures** — uploaded images with captions
+
+Inline-embed tokens (`[[diamond:ID]]`, `[[ach:ID]]`, `[[figure:ID]]`, and a
+bare `[[attack]]` coverage matrix) resolve in the live preview, the published
+web view, and the PDF. Tokens are notebook-scoped — a cross-notebook or
+unknown id degrades to an "unavailable" notice rather than leaking content.
+
+## Production: authoring and review { #author }
+
+One or more **reports** (intelligence products) are authored from a notebook's
+material in a full-height three-pane editor: markdown, live preview, and a
+docked panel for citations, tags, requirement links, and rendering.
+
+
+
+Reports carry:
+
+- an **intelligence level** — Strategic, Tactical, or Operational — used for
+ dissemination matching;
+- a **TLP marking** — a display and routing marking, never an in-portal read
+ gate;
+- **citations** of notebook sources and attachments, rendered as numbered
+ references;
+- **cited indicators**, rendered as an Indicators appendix;
+- **taxonomy tags** — threat actor, campaign, malware, ATT&CK technique,
+ sector, topic — driving faceted search, entity profiles, and an ATT&CK
+ coverage heatmap;
+- an advisory **estimative-language lint** (tradecraft nudges, never a block).
+
+Products render on demand to **branded PDFs** via Typst in three formats —
+full assessment, executive brief, one-pager — independent of intel level.
+
+Reports move Draft → In review → Approved → **Published**. Publishing freezes
+an immutable **publication snapshot**: stakeholder views, PDFs and MISP pushes
+all serve from the snapshot, never from live notebook material.
+
+
+
+## Dissemination: the stakeholder feed { #disseminate }
+
+On publish, a report is matched to stakeholders by their **preferred
+intelligence level** and **TLP ceiling** and delivered to a personal feed,
+with optional email notification and tag-subscription webhooks. Each feed item
+explains *why* it was delivered — an answered RFI and a preference match are
+reported separately.
+
+
+
+## Feedback: closing the loop
+
+On a delivered product, a stakeholder leaves **feedback**: a usefulness
+rating, an optional **RFI-satisfaction verdict** against one of their own
+requirements, and a comment. A *Met* verdict from the owning stakeholder
+auto-advances that requirement to Satisfied — closing the cycle. Feedback
+surfaces on the report for authors and on the requirement for analysts, and
+rolls up into a writer-only **program maturity dashboard** of CTI-CMM-style
+health indicators.
diff --git a/website/zensical.toml b/website/zensical.toml
new file mode 100644
index 0000000..aa3a2aa
--- /dev/null
+++ b/website/zensical.toml
@@ -0,0 +1,128 @@
+# IcebergCTI documentation site — Zensical (Material-based) configuration.
+# Styled to the shared Iceberg design system via docs/stylesheets/iceberg.css.
+
+[project]
+site_name = "Iceberg"
+site_description = "A cyber threat intelligence platform for collecting intelligence, authoring finished products, and disseminating them to stakeholders."
+site_author = "IcebergAI"
+site_url = "https://icebergai.github.io/IcebergCTI/"
+
+repo_url = "https://github.com/IcebergAI/IcebergCTI"
+repo_name = "IcebergAI/IcebergCTI"
+
+copyright = """
+Copyright © 2026 IcebergAI · License
+"""
+
+# Self-hosted fonts first, then the Iceberg token map + component overrides.
+extra_css = [
+ "stylesheets/fonts.css",
+ "stylesheets/iceberg.css",
+]
+
+nav = [
+ { "Home" = "index.md" },
+ { "The intelligence cycle" = "workflow.md" },
+ { "Deployment" = "deployment.md" },
+ { "Security" = "security.md" },
+ { "Integrations" = "integrations.md" },
+ { "Changelog" = "changelog.md" },
+]
+
+# ----------------------------------------------------------------------------
+# Theme
+# ----------------------------------------------------------------------------
+[project.theme]
+name = "material"
+logo = "assets/icebergai-mark.svg" # brand chip, reads on the dark header
+favicon = "assets/favicon.png"
+font = false # disable Google Fonts; we self-host (see fonts.css)
+
+features = [
+ "announce.dismiss",
+ "content.code.annotate",
+ "content.code.copy",
+ "content.code.select",
+ "content.tabs.link",
+ "content.tooltips",
+ "navigation.footer",
+ "navigation.indexes",
+ "navigation.instant",
+ "navigation.instant.prefetch",
+ "navigation.path",
+ "navigation.sections",
+ "navigation.top",
+ "navigation.tracking",
+ "search.highlight",
+ "toc.follow",
+]
+
+# Light / dark toggle mirroring the app's dual theme. primary/accent = "custom"
+# so the actual colours come from the --md-* overrides in iceberg.css.
+[[project.theme.palette]]
+scheme = "default"
+primary = "custom"
+accent = "custom"
+toggle.icon = "lucide/sun"
+toggle.name = "Switch to dark mode"
+
+[[project.theme.palette]]
+scheme = "slate"
+primary = "custom"
+accent = "custom"
+toggle.icon = "lucide/moon"
+toggle.name = "Switch to light mode"
+
+[[project.extra.social]]
+icon = "fontawesome/brands/github"
+link = "https://github.com/IcebergAI/IcebergCTI"
+
+# ----------------------------------------------------------------------------
+# Markdown extensions (carried over from the Zensical default template)
+# ----------------------------------------------------------------------------
+[project.markdown_extensions.abbr]
+[project.markdown_extensions.admonition]
+[project.markdown_extensions.attr_list]
+[project.markdown_extensions.def_list]
+[project.markdown_extensions.footnotes]
+[project.markdown_extensions.md_in_html]
+[project.markdown_extensions.toc]
+permalink = true
+[project.markdown_extensions.pymdownx.arithmatex]
+generic = true
+[project.markdown_extensions.pymdownx.betterem]
+[project.markdown_extensions.pymdownx.caret]
+[project.markdown_extensions.pymdownx.details]
+[project.markdown_extensions.pymdownx.emoji]
+emoji_generator = "zensical.extensions.emoji.to_svg"
+emoji_index = "zensical.extensions.emoji.twemoji"
+[project.markdown_extensions.pymdownx.highlight]
+anchor_linenums = true
+line_spans = "__span"
+pygments_lang_class = true
+[project.markdown_extensions.pymdownx.inlinehilite]
+[project.markdown_extensions.pymdownx.keys]
+[project.markdown_extensions.pymdownx.magiclink]
+[project.markdown_extensions.pymdownx.mark]
+[project.markdown_extensions.pymdownx.smartsymbols]
+# Pull the repo-root CHANGELOG.md into changelog.md via `--8<-- "CHANGELOG.md"`
+# (base_path reaches out of website/ to the repo root; check_paths fails the
+# build if the file moves rather than silently emitting an empty page).
+[project.markdown_extensions.pymdownx.snippets]
+base_path = [".."]
+check_paths = true
+[project.markdown_extensions.pymdownx.superfences]
+# NOTE: the mermaid fence is configured for parity with the sibling IcebergTTX/EBS
+# sites, but no page currently uses one — the theme lazy-loads mermaid.min.js from
+# **unpkg.com**, and every other asset on this site is self-hosted. Adding a
+# ```mermaid fence silently reintroduces a third-party CDN dependency at view
+# time; render the diagram as a table or an inline SVG instead.
+custom_fences = [
+ { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" }
+]
+[project.markdown_extensions.pymdownx.tabbed]
+alternate_style = true
+combine_header_slug = true
+[project.markdown_extensions.pymdownx.tasklist]
+custom_checkbox = true
+[project.markdown_extensions.pymdownx.tilde]