diff --git a/docs/superpowers/reports/2026-09-27-fastblocks-dogfood-readiness-verify.md b/docs/superpowers/reports/2026-09-27-fastblocks-dogfood-readiness-verify.md
index 9eb3ff3..875db87 100644
--- a/docs/superpowers/reports/2026-09-27-fastblocks-dogfood-readiness-verify.md
+++ b/docs/superpowers/reports/2026-09-27-fastblocks-dogfood-readiness-verify.md
@@ -38,21 +38,21 @@ version: 1
 ## Summary table
 
 | Dim | Status | Evidence | Followups |
 |-----|--------|----------|-----------|
 | D0  | PASS | `.verify-evidence/d0-backup-files.txt`, `.verify-evidence/d0-sites-fastest.txt` | none |
 | D1  | PASS | `.verify-evidence/d1-*.json` (4 coverage reports) | none (gap: spec 85% target vs current 67.81% floor) |
 | D2  | **PASS** | `.verify-evidence/d2-mypy.txt` (mypy 0 errors after Task 2; ty 0 errors after Task 1 collapse) | none (F1.5-D2-T1 [ADDRESSED] via `affe261`; F1.5-D2-T2 [RESCOLLAPSED] via Phase 1.5 recheck) |
 | D3  | **FAIL** | `.verify-evidence/d3-adapter-matrix.html`, `.verify-evidence/d3-status-code.txt` | F1.5-D3-T1 (adapter matrix surface), F1.5-D3-T2 (boot-test file inventory) |
 | D4  | PASS | `.verify-evidence/d4-demo-page.html`, `.verify-evidence/d4-demo-swap.html`, `.verify-evidence/d4-framework-htmx.txt` | HX-Trigger header noted (see D4 evidence) |
 | D5  | PASS | `.verify-evidence/d5-framework-async.txt`, `.verify-evidence/d5-htmy-snapshot.txt` | none |
-| D6  | **FAIL** | `.verify-evidence/d6-middleware.txt`, `.verify-evidence/d6-pip-audit.txt`, `.verify-evidence/d6-landing-security.txt` | F1.5-D6-T1 (CSP `unsafe-inline`), F1.5-D6-T2 (urllib3 CVEs), F1.5-D6-T3 (msgpack CVE) |
+| D6  | **PASS** | `.verify-evidence/d6-middleware.txt`, `.verify-evidence/d6-pip-audit.txt`, `.verify-evidence/d6-landing-security.txt`, `.verify-recheck/d6-csp-tests.txt` | F1.5-D6-T1 [ADDRESSED] via `0128432` (CSP nonce). F1.5-D6-T2..T5 were RESCOLLAPSED in Phase 1.5 recheck (original CVEs were in pip-audit's own tool venv, not fastblocks). |
 | D7  | PASS | `.verify-evidence/d7-performance.html`, `.verify-evidence/d7-perf.txt` | none |
 | D8  | **FAIL** | `.verify-evidence/d8-uv-lock-check.txt`, `.verify-evidence/d8-loose-pins.txt` | F1.5-D8-T1 (uv lock drift), F1.5-D8-T2 (typer/uvicorn/structlog upper caps) |
 | D9  | PASS | `.verify-evidence/d9-lychee.txt`, `.verify-evidence/d9-no-claim.txt`, `.verify-evidence/d9-residue.txt`, `.verify-evidence/d9-claim-gaps.txt` | kelp/webawesome residue is in archived plan files (historical context, legitimate) |
 | D12 | PASS | `.verify-evidence/d12-cross-deliverable-smoke.txt` | none |
 | D13 | PASS | `.verify-evidence/d13-framework-drift-diff.txt`, `.verify-evidence/d13-new-drift.txt` | none (no new F-FW-N+ entries) |
 
 ## Per-dimension evidence
 
 ### D0 — Housekeeping
 
@@ -125,40 +125,33 @@ All four coverages ≥ 67.81% floor. Framework ratchet at 67.98% is above the 67
 
 ### D5 — Async rendering
 
 **Status: PASS**
 
 - Framework async rendering perf: `2 passed`
 - HTMY-hybrid snapshot tests: `10 passed` (including `test_all_three_modes_semantically_equivalent` which proves Jinja/HTMY/Hybrid render semantically equivalent markup)
 
 ### D6 — Security
 
-**Status: FAIL**
+**Status: PASS**
 
-- Middleware tests: `11 passed, 2 skipped, 1 failed`. Failed test: `tests/middleware/test_security_headers.py::test_csp_header_present_and_safe` — CSP `style-src 'self' https: 'unsafe-inline'` allows `unsafe-inline` without a nonce. CSP is emitted as: `default-src 'self'; ... style-src 'self' https: 'unsafe-inline'; ...`
+- Middleware tests: `71 passed, 1 skipped` (full `tests/middleware/` + `tests/test_middleware*.py` after F1.5-D6-T1 landed). The originally-failing `tests/middleware/test_security_headers.py::test_csp_header_present_and_safe` now PASSES — CSP no longer allows `'unsafe-inline'` without a nonce. New file `tests/security/test_csp_no_unsafe_inline.py` adds 9 dedicated tests pinning the value shape (style-src strips `'unsafe-inline'`, nonce present in CSP, nonce varies per request, `inline_css(nonce)` emits the attribute, `build_nonce_csp` strips `'unsafe-inline'`, template-globals inline resolvers are callables). Evidence: `.verify-recheck/d6-csp-tests.txt`.
 - CSRF tests: passed
 - Autoescape regression: passed
 - `docs/security/auth-adapter-threat-model.md`: **EXISTS** ✓
-- **pip-audit:** 18 vulnerabilities in 4 packages. OSV.dev severity lookup:
-  - **HIGH**: `urllib3` (CVE-2026-44431, CVE-2026-44432) — fix to 2.7.0
-  - **HIGH**: `msgpack` (CVE-2026-57585) — fix to 1.2.1
-  - **MODERATE**: `idna` (CVE-2026-45409) — fix to 3.15
-  - **pip** self-vulnerabilities: 11 entries (CVE-2026-1703, CVE-2026-3219, CVE-2026-6357, CVE-2026-8643, CVE-2026-13346) — fix to 26.x
+- **pip-audit recheck (Phase 1.5):** ran `pip-audit --disable-pip --no-deps --requirement .verify-recheck/d6-fastblocks-frozen-requirements.txt` (uv-managed venv has no pip, so used `--disable-pip --no-deps --requirement` against a `uv pip freeze` snapshot of fastblocks `.venv`'s 309 packages). Returned `No known vulnerabilities found` (exit 0). All 18 original CVEs were in pip-audit's own tool environment (`/Users/les/.local/share/uv/tools/pip-audit/bin/python`), NOT fastblocks. Evidence: `.verify-recheck/d6-pip-audit-recheck.txt`, `.verify-recheck/d6-fastblocks-frozen-requirements.txt`.
 
-**Classification:** Per the brief: "FAIL if any CVEs are high-severity OR any test errors." Both triggers fire.
+**Classification:** Per the brief: "FAIL if any CVEs are high-severity OR any test errors." Both triggers are now absent in fastblocks `.venv`. PASS.
 
 **Phase 1.5 followups opened:**
-- **F1.5-D6-T1** — Fix CSP `style-src` to either drop `'unsafe-inline'` or use nonces/hashes
-- **F1.5-D6-T2** — Bump `urllib3` to ≥2.7.0 (closes 2 HIGH CVEs)
-- **F1.5-D6-T3** — Bump `msgpack` to ≥1.2.1 (closes 1 HIGH CVE)
-- **F1.5-D6-T4** (optional) — Bump `idna` to ≥3.15 (MODERATE)
-- **F1.5-D6-T5** (optional) — Refresh pip in venv (closes 11 self-vulns)
+- **F1.5-D6-T1** — [ADDRESSED] via `0128432` (CSP nonce-based). `SecureHeadersMiddleware` generates a per-request `secrets.token_urlsafe(16)` nonce, stores it on `scope["state"]["csp_nonce"]`, and overrides the `secure` library CSP to drop `'unsafe-inline'` from `style-src` while adding `'nonce-{value}'` to both `style-src` and `script-src`. `inline_css(nonce)` / `inline_js(nonce)` emit `<style nonce="...">` / `<script type="module" nonce="...">`. `template_globals()` returns callables (`_resolve_inline_css` / `_resolve_inline_js`) that look up the nonce from `get_request()` at render time. Note: the brief targeted `fastblocks/middleware/security.py` — that file does not exist; the actual CSP construction lives in `SecureHeadersMiddleware` inside `fastblocks/middleware.py`.
+- F1.5-D6-T2..T5 — RESCOLLAPSED (Phase 1.5 recheck found the 18 CVEs were in pip-audit's own tool venv, not fastblocks — see D6 evidence above).
 
 ### D7 — Performance
 
 **Status: PASS**
 
 - Landing `/performance`: `200`, 633 bytes
 - Framework perf benchmarks: `8 passed` (brotli 4, caching 2, minification 2). No regressions >10% from seeded `.benchmarks/` data (benchmarks showed stable timing across runs).
 
 ### D8 — Deps
 
@@ -204,21 +197,21 @@ All 12 routes returned 200:
 (Per spec integration contract: failing dimensions re-open as Phase 1.5 followups, NOT Phase 2 patches. Phase 2 is frozen at `741e4d6`.)
 
 | ID | Dim | Description |
 |---|---|---|
 | F1.5-D1-T1 | D1 | **Heaviest followup** — bump `.coverage-ratchet.json` floor to 85% per spec §D1 line 319 (current floor 67.81% is a 17-percentage-point gap). Multi-week effort, likely requires additional test authoring across framework + starters + examples. Schedule last in Phase 1.5 sequence. |
 | F1.5-D2-T1 | D2 | [ADDRESSED] Fixed 7 mypy errors across 5 framework files (templates/__init__.py, resolver.py, sitemap/core.py, hybrid.py, starters/default/mcp/server.py). Receiver-side `Mapping[str, object] \| None` change collapsed variance notes in home.py/demo.py at the call sites. mypy 0 errors after commit `affe261`. |
 | F1.5-D2-T2 | D2 | [RESCOLLAPSED] Recheck via `ty check --python /Users/les/Projects/fastblocks/.venv/bin/python fastblocks` (note: brief's `PIPAPI_PYTHON_LOCATION` env var is pip-audit-only; ty's native `--python` flag is the correct venv selector) returned `All checks passed!` (0 errors, 0 warnings) against fastblocks `.venv/lib/python3.14/site-packages`. Verbose log confirms ty resolved site-packages to `/Users/les/Projects/fastblocks/.venv/lib/python3.14/site-packages` (not mahavishnu). All 95 prior diagnostics were measurement artifacts. Evidence: `.verify-recheck/d2-ty-recheck.txt`. |
 | F1.5-D3-T1 | D3 | Fix adapter-matrix route to enumerate spec §D3 in-scope adapters |
 | F1.5-D3-T2 | D3 | Add `tests/adapters/<domain>/<key>/test_boot.py` for matrix adapters |
 | F1.5-D4-T1 | D4 | Investigate `HX-Trigger` header emission in `/demo` HTMX swap (absent in `d4-demo-swap.html`; brief required HX-Trigger presence as part of the swap evidence chain; implementer classified as feature gap not correctness bug — adjudicated as Phase 1.5 followup) |
-| F1.5-D6-T1 | D6 | Fix CSP `style-src` to drop `'unsafe-inline'` or use nonces |
+| F1.5-D6-T1 | D6 | [ADDRESSED] via `0128432` (CSP nonce-based: `SecureHeadersMiddleware` now generates a per-request `secrets.token_urlsafe(16)` nonce, stores it on `scope["state"]["csp_nonce"]`, and overrides the `secure` library CSP to drop `'unsafe-inline'` from `style-src` while adding `'nonce-{value}'` to both `style-src` and `script-src`. `inline_css(nonce)` / `inline_js(nonce)` now emit `<style nonce="...">` / `<script type="module" nonce="...">`. `template_globals()` now returns callables (`_resolve_inline_css` / `_resolve_inline_js`) that look up the nonce from `get_request()` at render time so the `<style>` / `<script>` tag matches the per-request CSP. 9 new tests in `tests/security/test_csp_no_unsafe_inline.py` pin the value shape: `'unsafe-inline'` absent from `style-src`, nonce present, nonce varies per request, `inline_css(nonce)` emits the attribute, `build_nonce_csp` strips `'unsafe-inline'`. Existing `tests/middleware/test_security_headers.py::test_csp_header_present_and_safe` now passes (was the original failing test); all 71 middleware tests pass with no regression.) |
 | F1.5-D6-T2 | D6 | [RESCOLLAPSED] Recheck via `pip-audit --disable-pip --no-deps --requirement .verify-recheck/d6-fastblocks-frozen-requirements.txt` (note: brief's `PIPAPI_PYTHON_LOCATION` requires pip inside the target venv; fastblocks `.venv` is uv-managed with no pip, so used `--disable-pip --no-deps --requirement` with a `uv pip freeze --python .venv/bin/python` snapshot of 309 packages) returned `No known vulnerabilities found` (exit 0). All 18 prior CVEs were in pip-audit's own tool environment (`/Users/les/.local/share/uv/tools/pip-audit/bin/python`), NOT fastblocks. The original d6-pip-audit.txt even contains a pip-audit warning: *"will run pip against /Users/les/.local/share/uv/tools/pip-audit/bin/python, but you have a virtual environment loaded at /Users/les/Projects/mahavishnu/.venv"*. Evidence: `.verify-recheck/d6-pip-audit-recheck.txt`, `.verify-recheck/d6-fastblocks-frozen-requirements.txt`. |
 | F1.5-D6-T3 | D6 | [RESCOLLAPSED] msgpack 1.1.2 is NOT in fastblocks `.venv`. Recheck (`uv pip freeze --python .venv/bin/python`) shows fastblocks venv has zero msgpack install. Original CVE-2026-57585 was in pip-audit's own tool environment, not fastblocks. See F1.5-D6-T2 evidence. |
 | F1.5-D6-T4 | D6 | [RESCOLLAPSED] idna 3.11 is NOT in fastblocks `.venv`. Recheck (`uv pip freeze --python .venv/bin/python`) shows fastblocks venv has zero idna install. Original CVE-2026-45409 was in pip-audit's own tool environment, not fastblocks. See F1.5-D6-T2 evidence. |
 | F1.5-D6-T5 | D6 | [RESCOLLAPSED] pip self-vulns (PYSEC-2026-1796/2875/2876/196/3721) were against pip-audit's bundled pip 25.3 in `/Users/les/.local/share/uv/tools/pip-audit/bin/python`. fastblocks `.venv` is uv-managed and does not install pip at all (verified: `.venv/bin/python -m pip --version` → `No module named pip`). The 11 self-vulns cannot apply to a venv that has no pip. See F1.5-D6-T2 evidence. |
 | F1.5-D8-T1 | D8 | Resolve uv lock drift (user-controlled) |
 | F1.5-D8-T2 | D8 | Add upper caps to `typer`, `uvicorn`, `structlog` |
 | F1.5-DELIV-T1 | DELIV | B1 starter's `fastblocks run` CLI subcommand does not exist (per `examples/landing/main.py:14` docstring, actual entry is `uv run fastblocks run` or `uvicorn main:app`). The "one-command install + run" claim (per spec §B1) is broken. Either implement the `run` subcommand in `fastblocks/cli.py` OR update starter README + starter `pyproject.toml` scripts section to use `uvicorn main:app` consistently. |
 | F1.5-F-FW-1 | FW | `fastblocks.adapters.templates.hybrid.HybridTemplatesManager.render_hybrid()` does not exist (per D13 evidence). B3 htmy-hybrid demo currently fakes hybrid rendering. Add the API OR document the limitation and remove the B3 demo's claim of true parity. |
 
 **Dependency annotations**:
diff --git a/fastblocks/adapters/templates/htmy_components/adapter.py b/fastblocks/adapters/templates/htmy_components/adapter.py
index db28e7d..51580e5 100644
--- a/fastblocks/adapters/templates/htmy_components/adapter.py
+++ b/fastblocks/adapters/templates/htmy_components/adapter.py
@@ -8,27 +8,30 @@ Adapted from the standalone ``fastblocks-htmy`` 0.5.0 PyPI package
 caller is now inside fastblocks itself — the resolved adapter is
 ``fastblocks.adapters.templates.htmy.HTMYTemplates``.
 
 FastBlocks templates use ``[[ ... ]]`` delimiters (not Jinja's ``{{ ... }}``).
 This adapter exposes:
 
 - the on-disk paths of the shipped ``fastblocks-ui`` CSS/JS bundle, so the app can
   mount them as a static route (resolved via ``importlib.resources``, no copy);
 - cache-busted asset URLs keyed to the installed ``fastblocks-ui`` version, for apps
   that want the CSS served as a separate, browser-cacheable ``<link>``;
-- ``inline_css()`` / ``inline_js()`` (and the ``fastblocks_ui_css_inline`` /
+- ``inline_css(nonce)`` / ``inline_js(nonce)`` (and the ``fastblocks_ui_css_inline`` /
   ``fastblocks_ui_js_inline`` template globals), which embed the actual CSS/JS in
   ``<style>``/``<script type="module">`` tags instead -- the recommended default for
   htmx apps, since it avoids an extra request without needing a static mount at all.
-  Both require the app's CSP (if any) to allow inline ``style-src``/``script-src``
-  (a nonce or hash, or ``'unsafe-inline'``) -- if that's not an option, use the
-  link/script-src URL globals below instead. ``enhance.js``'s own setup code is
+  Both accept an optional ``nonce`` so the tag matches a per-request CSP nonce --
+  ``SecureHeadersMiddleware`` emits ``style-src 'self' 'nonce-...'`` /
+  ``script-src 'self' 'nonce-...'`` (F1.5-D6-T1 in
+  ``docs/superpowers/plans/2026-09-27-fastblocks-dogfood-readiness-phase1.5.md``),
+  and the template globals pick the nonce up from the ASGI scope at render time.
+  ``enhance.js``'s own setup code is
   idempotent (custom-element registration checks ``customElements.get()`` first;
   auto-boot checks ``window.fastBlocksUI`` first), so re-inlining it more than once
   on the same page is a safe no-op, not a crash -- but it should still only need to
   appear once, in the base layout, the same as the CSS;
 - a set of template globals (the htmy component classes plus the string helpers)
   to register once on the FastBlocks/Jinja environment, for the plain-Jinja-global
   usage path (``[[ ui_button(...) ]]`` — verified working, see
   ``tests/test_fastblocks_integration.py``); and
 - ``trusted_components()`` / ``register_with_htmy_adapter()`` for the typed,
   ``render_component()``-mediated usage path (``[[ render_component("button", {...}) ]]``),
@@ -52,25 +55,25 @@ Example (FastBlocks/Starlette-style)::
 
     # During app startup, once the FastBlocks htmy adapter is importable:
     from fastblocks.adapters.templates.htmy import HTMYTemplates
 
     await register_with_htmy_adapter(HTMYTemplates())
 
 Then in a FastBlocks template's base layout (recommended -- inline, no extra request,
 no static mount needed)::
 
     <head>
-    [[ fastblocks_ui_css_inline ]]
+    [[ fastblocks_ui_css_inline() ]]
     </head>
     [[ ui_button("Save", variant="primary") ]]
     [[ render_component("button", {"text": "Save", "variant": "primary"}) ]]
-    [[ fastblocks_ui_js_inline ]]
+    [[ fastblocks_ui_js_inline() ]]
 """
 
 from __future__ import annotations
 
 from pathlib import Path
 from typing import Any
 
 import fastblocks_ui
 from fastblocks_ui import SafeHTML
 
@@ -184,68 +187,122 @@ def asset_urls(
     *, mount: str = "/static/fastblocks-ui", cache_bust: bool = True
 ) -> dict[str, str]:
     """Return cache-busted static URLs for the assets under ``mount``."""
     suffix = f"?v={fastblocks_ui.__version__}" if cache_bust else ""
     return {
         "css": f"{mount}/css/fastblocks-ui.css{suffix}",
         "js": f"{mount}/js/enhance.js{suffix}",
     }
 
 
-def inline_css() -> SafeHTML:
+def inline_css(nonce: str | None = None) -> SafeHTML:
     """Return the shipped fastblocks-ui CSS bundle wrapped in a ``<style>`` tag.
 
     Reads fresh from disk on every call (no caching here -- the app's own
     response/page cache is the right layer for that).
 
     Marked ``SafeHTML`` (implements ``__html__``) so it renders unescaped through
     a ``[[ ... ]]`` Jinja global the same way the typed htmy components already
     do -- see ``fastblocks_ui.helpers._render_fragment``. Reading the actual CSS
     on every call (instead of embedding a string literal at import time) means
     this can never drift from the installed fastblocks-ui version, the same
     property ``asset_paths()``/``asset_urls()`` already have.
+
+    Pass ``nonce`` to emit ``<style nonce="...">``; required when the app's
+    CSP forbids inline styles (``style-src 'self' 'nonce-...'; ...`` --
+    see F1.5-D6-T1 in
+    ``docs/superpowers/plans/2026-09-27-fastblocks-dogfood-readiness-phase1.5.md``).
+    Without ``nonce`` the tag is emitted bare -- fine for legacy CSPs that
+    still allow ``'unsafe-inline'``.
     """
     css = Path(fastblocks_ui.get_css_path()).read_text(encoding="utf-8")
-    return SafeHTML(f"<style>\n{css}\n</style>")
+    nonce_attr = f' nonce="{nonce}"' if nonce else ""
+    return SafeHTML(f"<style{nonce_attr}>\n{css}\n</style>")
 
 
-def inline_js() -> SafeHTML:
+def inline_js(nonce: str | None = None) -> SafeHTML:
     """Return the shipped fastblocks-ui enhancement JS wrapped in a ``<script type="module">`` tag.
 
     Reads fresh from disk on every call -- same rationale as ``inline_css()``.
 
     Deliberately reads ``static/js/enhance.js`` directly rather than
     ``fastblocks_ui.get_js_path()`` (which points at ``fastblocks-ui.js``, a
     thin wrapper that re-exports from ``enhance.js`` via a *relative* ES
     import). That relative import only resolves when the wrapper is loaded
     as its own external file sitting next to ``enhance.js`` on disk -- inlined
     into an arbitrary page, ``./enhance.js`` would resolve against the page's
     own URL and 404 in virtually any real deployment. ``asset_urls()["js"]``
     already made the same choice, linking straight to ``enhance.js``.
 
     Safe to inline: ``enhance.js``'s setup code is idempotent by construction
     (custom-element registration checks ``customElements.get(name)`` before
     calling ``define()``; the auto-boot path checks ``window.fastBlocksUI``
     before running init), so if this ever ends up on the page more than once,
     re-running it is a harmless no-op rather than a thrown
     ``NotSupportedError`` or duplicate event listeners.
+
+    Pass ``nonce`` to emit ``<script type="module" nonce="...">``; required
+    when the app's CSP forbids inline scripts.
     """
     static_root = Path(fastblocks_ui.get_static_path())
     js = (static_root / "js" / "enhance.js").read_text(encoding="utf-8")
-    return SafeHTML(f'<script type="module">\n{js}\n</script>')
+    nonce_attr = f' nonce="{nonce}"' if nonce else ""
+    return SafeHTML(f'<script type="module"{nonce_attr}>\n{js}\n</script>')
+
+
+def _resolve_inline_css() -> SafeHTML:
+    """Template-global callable: returns ``inline_css`` for the current request's CSP nonce.
+
+    Looks up the per-request nonce from the active ASGI scope (set by
+    ``SecureHeadersMiddleware``) so the emitted ``<style>`` tag matches
+    the nonce-bearing CSP. Falls back to a bare ``<style>`` when no
+    nonce is present (e.g. during template rendering outside an HTTP
+    request, or for callers who haven't enabled strict CSP).
+    """
+    from fastblocks.middleware import get_request
+
+    request_scope = get_request()
+    if isinstance(request_scope, dict):
+        state = request_scope.get("state")
+        if isinstance(state, dict):
+            nonce = state.get("csp_nonce")
+            if isinstance(nonce, str) and nonce:
+                return inline_css(nonce)
+    return inline_css()
+
+
+def _resolve_inline_js() -> SafeHTML:
+    """Template-global callable: returns ``inline_js`` for the current request's CSP nonce."""
+    from fastblocks.middleware import get_request
+
+    request_scope = get_request()
+    if isinstance(request_scope, dict):
+        state = request_scope.get("state")
+        if isinstance(state, dict):
+            nonce = state.get("csp_nonce")
+            if isinstance(nonce, str) and nonce:
+                return inline_js(nonce)
+    return inline_js()
 
 
 def template_globals() -> dict[str, object]:
     """Globals to register on a FastBlocks/Jinja (``[[ ]]``) environment.
 
     Exposes both the typed htmy component classes and the zero-dependency string
     helpers (handy for quick fragments), plus ready-made asset URLs.
+
+    The two inline globals ``fastblocks_ui_css_inline`` and
+    ``fastblocks_ui_js_inline`` are CALLABLES, not values -- the
+    framework picks up the per-request CSP nonce from the ASGI scope so
+    the emitted ``<style>`` / ``<script>`` tag carries the matching
+    nonce. Invoke them in templates as ``[[ fastblocks_ui_css_inline() ]]``
+    (parens required -- Jinja does not auto-call global callables).
     """
     urls = asset_urls()
     return {
         # Typed htmy components
         "Alert": Alert,
         "Breadcrumb": Breadcrumb,
         "Button": Button,
         "Card": Card,
         "Checkbox": Checkbox,
         "Column": Column,
@@ -277,14 +334,16 @@ def template_globals() -> dict[str, object]:
         "ValidationSummary": ValidationSummary,
         # String helpers for quick fragments
         "ui_button": fastblocks_ui.button,
         "ui_card": fastblocks_ui.card,
         "ui_field": fastblocks_ui.field,
         "ui_alert": fastblocks_ui.alert,
         # Asset URLs (link/script-src-based, browser-cacheable -- see inline_css()/
         # inline_js() for the recommended alternative)
         "fastblocks_ui_css": urls["css"],
         "fastblocks_ui_js": urls["js"],
-        # Inline CSS/JS (recommended default -- see module docstring)
-        "fastblocks_ui_css_inline": inline_css(),
-        "fastblocks_ui_js_inline": inline_js(),
+        # Inline CSS/JS (recommended default -- see module docstring).
+        # These are callables; templates must invoke them with ``()``
+        # so the per-request CSP nonce can be picked up at render time.
+        "fastblocks_ui_css_inline": _resolve_inline_css,
+        "fastblocks_ui_js_inline": _resolve_inline_js,
     }
diff --git a/fastblocks/middleware.py b/fastblocks/middleware.py
index 661092f..fea9696 100644
--- a/fastblocks/middleware.py
+++ b/fastblocks/middleware.py
@@ -1,10 +1,11 @@
+import secrets
 import sys
 import typing as t
 from collections.abc import Callable, Mapping, Sequence
 from contextvars import ContextVar
 from enum import IntEnum
 
 # Oneiric imports
 from oneiric.core.logging import get_logger
 from fastblocks.core.resolver import FastblocksRegistry, get_resolver
 
@@ -184,37 +185,104 @@ class CurrentRequestMiddleware:
             MiddlewareUtils.HTTP,
             MiddlewareUtils.WEBSOCKET,
         ):
             await self.app(scope, receive, send)
             return
         local_scope = _request_ctx_var.set(scope)
         await self.app(scope, receive, send)
         _request_ctx_var.reset(local_scope)
 
 
+def build_nonce_csp(nonce: str, library_csp: str) -> str:
+    """Rebuild the CSP string with nonce-bearing style-src and script-src.
+
+    The ``secure.with_default_headers()`` profile emits something like:
+        ``default-src 'self'; ... style-src 'self' https: 'unsafe-inline'; script-src 'self'; ...``
+
+    We split on ``;`` and rebuild the two affected directives so every
+    other directive (``default-src``, ``base-uri``, ``font-src``,
+    ``img-src``, ``frame-ancestors``, ``object-src``, ...) is preserved
+    verbatim. ``'unsafe-inline'`` is stripped from ``style-src``;
+    ``style-src`` and ``script-src`` both gain a nonce source expression.
+
+    See F1.5-D6-T1 in
+    ``docs/superpowers/plans/2026-09-27-fastblocks-dogfood-readiness-phase1.5.md``
+    for the rationale.
+    """
+    if not nonce:
+        return library_csp
+    parts = [p.strip() for p in library_csp.split(";") if p.strip()]
+    rebuilt: list[str] = []
+    for directive in parts:
+        name, _, value = directive.partition(" ")
+        if name == "style-src":
+            tokens = [t for t in value.split() if t != "'unsafe-inline'"]
+            rebuilt.append(f"style-src {' '.join(tokens)} 'nonce-{nonce}'")
+        elif name == "script-src":
+            rebuilt.append(f"script-src {value} 'nonce-{nonce}'")
+        else:
+            rebuilt.append(directive)
+    return "; ".join(rebuilt)
+
+
 class SecureHeadersMiddleware:
+    """Emit security headers on every HTTP response.
+
+    The default ``secure.with_default_headers()`` profile sets
+    ``style-src 'self' https: 'unsafe-inline'`` -- the inline fallback is
+    what OWASP calls "permissive". We override the CSP to use a per-
+    request nonce instead: ``style-src 'self' 'nonce-{value}'`` and
+    ``script-src 'self' 'nonce-{value}'``. The nonce is stored on the
+    ASGI ``scope["state"]`` dict under ``csp_nonce`` so template helpers
+    (e.g. ``fastblocks.adapters.templates.htmy_components.inline_css``)
+    can pick it up and emit ``<style nonce="...">`` / ``<script nonce="...">``
+    tags that match the policy.
+
+    All other headers (HSTS, XFO, XCTO, Referrer-Policy, Permissions-
+    Policy, COOP/CORP) come straight from the ``secure`` library profile
+    -- they are already strict and do not depend on per-request state.
+    """
+
     def __init__(self, app: ASGIApp) -> None:
         self.app = app
         try:
             self.logger = get_logger("fastblocks")
         except (ImportError, AttributeError, RuntimeError):
             self.logger = None
 
     async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
         if scope["type"] != "http":
             return await self.app(scope, receive, send)
 
+        # Per-request nonce. 16 random bytes -> 22-char urlsafe token.
+        nonce = secrets.token_urlsafe(16)
+        # Starlette scopes use a plain dict under `state`. Lazy-create so
+        # we don't mutate shared state across requests.
+        state = scope.setdefault("state", {})
+        if isinstance(state, dict):
+            state["csp_nonce"] = nonce
+
         async def send_with_secure_headers(message: Message) -> None:
             if message["type"] == "http.response.start":
                 headers = MutableHeaders(scope=message)
                 for header_name, header_value in secure_headers.headers.items():
-                    headers.append(header_name, header_value)
+                    if header_name.lower() == "content-security-policy":
+                        # Override the library default -- drop
+                        # 'unsafe-inline' from style-src and inject the
+                        # nonce. Keep every other directive from the
+                        # library profile.
+                        headers.append(
+                            header_name,
+                            build_nonce_csp(nonce, header_value),
+                        )
+                    else:
+                        headers.append(header_name, header_value)
             await send(message)
 
         await self.app(scope, receive, send_with_secure_headers)
         return None
 
 
 class CacheValidator:
     def __init__(self, rules: Sequence[Rule] | None = None) -> None:
         self.rules = rules or [Rule()]
 
diff --git a/tests/security/test_csp_no_unsafe_inline.py b/tests/security/test_csp_no_unsafe_inline.py
new file mode 100644
index 0000000..171cfe3
--- /dev/null
+++ b/tests/security/test_csp_no_unsafe_inline.py
@@ -0,0 +1,193 @@
+"""F1.5-D6-T1: CSP ``style-src`` must not allow ``'unsafe-inline'``.
+
+Path A from the Phase 1.5 plan: ``SecureHeadersMiddleware`` overrides the
+``secure`` library default (which sets ``style-src 'self' https:
+'unsafe-inline'``) and rebuilds the CSP with a per-request nonce --
+``style-src 'self' https: 'nonce-...'``. ``'unsafe-inline'`` must NOT
+appear in the emitted header.
+
+This file complements the broader D6 contract in
+``tests/middleware/test_security_headers.py`` with a single-purpose
+check that pins the value shape of ``style-src`` specifically.
+
+The middleware-level assertion lives in ``test_security_headers.py``;
+this file adds nonce-shape coverage plus the helper-side guard
+(``inline_css(nonce)`` emits the matching attribute).
+"""
+
+from __future__ import annotations
+
+from unittest.mock import MagicMock
+
+import pytest
+from starlette.responses import PlainTextResponse
+from starlette.testclient import TestClient
+
+
+def _make_config(deployed: bool = False) -> MagicMock:
+    cfg = MagicMock()
+    cfg.deployed = deployed
+    cfg.debug = MagicMock(production=False)
+    cfg.app = MagicMock()
+    cfg.app.secret_key.get_secret_value.return_value = "x" * 32
+    cfg.app.token_id = "_fb_"
+    cfg.app.security_headers_strict = True
+    return cfg
+
+
+@pytest.fixture
+def client():
+    """Build a real FastBlocks-style app with the default middleware stack."""
+    from fastblocks.middleware import MiddlewareStackManager
+
+    cfg = _make_config(deployed=False)
+    mgr = MiddlewareStackManager(config=cfg)
+    mgr.initialize()
+    stack = mgr.build_stack()
+
+    async def home(scope, receive, send):  # noqa: ARG001
+        await send({"type": "http.response.start", "status": 200, "headers": []})
+        await send({"type": "http.response.body", "body": b"ok"})
+
+    # Wrap innermost first so the stack applies outermost last.
+    wrapped = home
+    for mw in reversed(stack):
+        cls = getattr(mw, "cls", mw)
+        kwargs = dict(getattr(mw, "kwargs", {}))
+        wrapped = cls(wrapped, **kwargs)
+
+    return TestClient(wrapped)
+
+
+def test_csp_style_src_excludes_unsafe_inline(client):
+    """``style-src`` must not allow ``'unsafe-inline'`` (F1.5-D6-T1)."""
+    r = client.get("/")
+    csp = next(
+        (v for k, v in r.headers.items() if k.lower() == "content-security-policy"),
+        None,
+    )
+    assert csp is not None, "Content-Security-Policy header missing"
+
+    # Extract the style-src directive.
+    directives = [d.strip() for d in csp.split(";") if d.strip()]
+    style_src = next((d for d in directives if d.startswith("style-src ")), "")
+    assert style_src, f"style-src directive missing in CSP: {csp!r}"
+    assert "'unsafe-inline'" not in style_src, (
+        f"F1.5-D6-T1: style-src allows 'unsafe-inline': {style_src!r}"
+    )
+
+
+def test_csp_includes_nonce(client):
+    """``SecureHeadersMiddleware`` must inject a nonce into style-src."""
+    r = client.get("/")
+    csp = next(
+        (v for k, v in r.headers.items() if k.lower() == "content-security-policy"),
+        None,
+    )
+    assert csp is not None
+    assert "nonce-" in csp, f"F1.5-D6-T1: CSP missing nonce source: {csp!r}"
+
+
+def test_csp_nonce_varies_per_request(client):
+    """Two requests get distinct nonces (no accidental caching)."""
+    r1 = client.get("/")
+    r2 = client.get("/")
+
+    def _nonce(csp: str) -> str:
+        for directive in csp.split(";"):
+            directive = directive.strip()
+            if directive.startswith("style-src ") or directive.startswith(
+                "script-src "
+            ):
+                for token in directive.split():
+                    if token.startswith("'nonce-"):
+                        return token
+        return ""
+
+    csp1 = next(
+        (v for k, v in r1.headers.items() if k.lower() == "content-security-policy"),
+        "",
+    )
+    csp2 = next(
+        (v for k, v in r2.headers.items() if k.lower() == "content-security-policy"),
+        "",
+    )
+    n1 = _nonce(csp1)
+    n2 = _nonce(csp2)
+    assert n1, "first CSP missing nonce"
+    assert n2, "second CSP missing nonce"
+    assert n1 != n2, f"F1.5-D6-T1: nonce reused across requests: {n1!r}"
+
+
+def test_inline_css_emits_nonce_when_provided():
+    """``inline_css(nonce)`` must emit ``<style nonce="...">``."""
+    from fastblocks.adapters.templates.htmy_components.adapter import inline_css
+
+    out = inline_css("abc123")
+    # SafeHTML wraps the string; check the underlying HTML.
+    html = str(out)
+    assert 'nonce="abc123"' in html, (
+        f"inline_css(nonce) did not emit nonce attribute: {html[:80]!r}"
+    )
+    assert html.startswith("<style nonce="), html[:80]
+
+
+def test_inline_css_no_nonce_omits_attribute():
+    """Without a nonce, ``inline_css()`` emits a bare ``<style>`` (legacy CSP compat)."""
+    from fastblocks.adapters.templates.htmy_components.adapter import inline_css
+
+    out = inline_css()
+    html = str(out)
+    assert "<style>" in html, html[:80]
+    assert "nonce=" not in html, html[:80]
+
+
+def test_build_nonce_csp_strips_unsafe_inline():
+    """``build_nonce_csp`` unit-level: drops ``'unsafe-inline'`` from style-src."""
+    from fastblocks.middleware import build_nonce_csp
+
+    library_csp = (
+        "default-src 'self'; style-src 'self' https: 'unsafe-inline'; "
+        "script-src 'self'; img-src 'self' data:"
+    )
+    out = build_nonce_csp("noncevalue", library_csp)
+    assert "'unsafe-inline'" not in out, out
+    assert "'nonce-noncevalue'" in out, out
+
+    # Every other directive preserved verbatim.
+    assert "default-src 'self'" in out, out
+    assert "img-src 'self' data:" in out, out
+
+
+def test_build_nonce_csp_empty_nonce_passes_through():
+    """When nonce is empty, ``build_nonce_csp`` returns the library CSP unchanged."""
+    from fastblocks.middleware import build_nonce_csp
+
+    library_csp = "style-src 'self' https: 'unsafe-inline'; script-src 'self'"
+    out = build_nonce_csp("", library_csp)
+    assert out == library_csp
+
+
+def test_inline_js_emits_nonce_when_provided():
+    """``inline_js(nonce)`` must emit ``<script type="module" nonce="...">``."""
+    from fastblocks.adapters.templates.htmy_components.adapter import inline_js
+
+    out = inline_js("xyz789")
+    html = str(out)
+    assert 'nonce="xyz789"' in html, html[:80]
+    assert html.startswith('<script type="module" nonce="'), html[:80]
+
+
+def test_template_globals_inline_resolvers_are_callables():
+    """The inline globals are callables so they can resolve the request nonce at render time."""
+    from fastblocks.adapters.templates.htmy_components.adapter import (
+        template_globals,
+    )
+
+    gl = template_globals()
+    assert callable(gl["fastblocks_ui_css_inline"]), (
+        "fastblocks_ui_css_inline must be a callable (Jinja globals don't auto-call)"
+    )
+    assert callable(gl["fastblocks_ui_js_inline"]), (
+        "fastblocks_ui_js_inline must be a callable (Jinja globals don't auto-call)"
+    )
\ No newline at end of file
