@passioncode-ai/fabric-agent-adapter 0.8.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +14 -3
  3. package/package.json +1 -1
  4. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  5. package/plugins/fabric-agent-adapter/hooks/hooks.json +17 -0
  6. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  8. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +21 -8
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/demo.html +110 -0
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +198 -0
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +253 -0
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +28 -0
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/themes.json +350 -0
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +133 -0
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +4 -3
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -3
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +7 -4
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +19 -2
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +295 -0
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +88 -5
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +437 -0
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +143 -17
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +195 -36
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample-remote-service.mjs +9 -1
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +29 -4
  26. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,105 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 - 2026-10-11
4
+
5
+ Contract DEC-0039, first part (FAA-20, issue #52): the operator's dashboard session now lives no longer than
6
+ the contract allows, and the server enforces it; the probe treats `surfaces.mcp.auth: "delegation"` like `own`.
7
+ The contract pin moves to Fabric Agent Contract `main` `8e49fbd` (the DEC-0039 merge, contract PR #30). Bundles
8
+ issued under the DEC-0036, DEC-0035, DEC-0033, DEC-0031, DEC-0024, DEC-0021, DEC-0020 and DEC-0019 revisions
9
+ stay valid.
10
+
11
+ ### Security
12
+
13
+ - **Operator sessions end on the server (DEC-0039).** Until now `LoginCodes.session_valid` checked only an HMAC
14
+ and the cookie lived 30 days, so a copied cookie stayed valid until the session key was rotated. Now a
15
+ session ends 12 h after its last use, 7 days after the login code was redeemed however often it is used, on
16
+ `logout`, and when the descriptor token rotates. The cookie carries a signed claim (session id, sign-in time,
17
+ token revision); the code store keeps a record per session (last use, logout), so a cookie with no record is
18
+ refused. Both kits, the same behaviour.
19
+
20
+ ### Changed
21
+
22
+ - **Every operator signs in once more after the upgrade**: a cookie of the old format is refused. The host does
23
+ it on the page's `401`.
24
+ - **A remote service with `MemoryCodeStore` loses its sessions on a deploy** (it already lost its login codes):
25
+ the store now holds the session records. Pass a durable store to keep them.
26
+ - **`session_cookie_header` / `remote_session_cookie_header`** (and the Node `sessionCookieHeader` /
27
+ `remoteSessionCookieHeader`) default to `Max-Age` 604800 (7 days), not 30 days.
28
+ - **Default contract pin `a83bec6` → `8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5`.** `a83bec6` (DEC-0036) joins
29
+ `SUPPORTED_CONTRACT_COMMITS`. The revisions between (DEC-0037 spend policy, DEC-0038 grant windows,
30
+ DEC-0039 delegation, DEC-0040 Fabric's button) add schemas and optional properties only: the provider
31
+ manifest gains optional cost presets, the governance grant's `effects` may be omitted by a delegation grant,
32
+ and the well-known document gains `auth.delegation` and the `delegation` value of `surfaces.mcp.auth`.
33
+ Every descriptor, well-known document and bundle the kits wrote before stays valid.
34
+
35
+ ### Added
36
+
37
+ - **`LoginCodes(..., token=, idle_seconds=, max_seconds=, clock=)`** (`{ token, idleSeconds, maxSeconds, clock }`
38
+ in Node): `token` is the descriptor token or a callable returning the current one; the bounds may be
39
+ shortened, and longer values are clamped. **`logout(cookie)`** and **`expired_session_cookie_header(remote=)`**
40
+ (`logout`, `expiredSessionCookieHeader`); `OPERATOR_SESSION_IDLE_SECONDS`, `OPERATOR_SESSION_MAX_SECONDS`.
41
+ - **`check_service.py`**: `surfaces.mcp.auth: "delegation"` is checked like `own` — the MCP surface must refuse
42
+ the descriptor token, and the caller rules are `NOT_RUN` (DL-16); rule **`auth.delegation-declared`** (with
43
+ `delegation`, the well-known document names `auth.delegation {issuer, jwks}`); rule
44
+ **`login.session-lifetime`** (the cookie set at sign-in does not outlive 7 days).
45
+ - **The sample services** pass `token=` and serve `POST /logout` (a browser write: it needs `X-Fabric-Request`).
46
+ - `references/surfaces-and-auth.md`, `remote-placement.md` and `protocol.md` describe the lifetime, logout and
47
+ DL-16. Scenario evals `operator-session-lifetime` and `mcp-auth-delegation` came first.
48
+
49
+ ### Not yet
50
+
51
+ - Verifying delegation tokens and approvals in the kits (`verifyDelegation` / `verifyApproval`) and Fabric's
52
+ button in the dashboard kit (FB-7) follow in a later release (FAA-20 stays open).
53
+
54
+ ## 0.9.0 - 2026-10-11
55
+
56
+ The dashboard kit (`fabric-dashboard/0.1`, contract DEC-0036): a Fabric service's dashboard holds inside a
57
+ host pane beside the agent console from 360 CSS px up, follows the host's colour scheme and theme, and says
58
+ so in its well-known document. The contract pin moves to Fabric Agent Contract `main` `a83bec6` (the
59
+ DEC-0036 merge, contract PR #27). Bundles issued under the DEC-0035, DEC-0033, DEC-0031, DEC-0024, DEC-0021,
60
+ DEC-0020 and DEC-0019 revisions stay valid.
61
+
62
+ ### Added
63
+
64
+ - **`building-fabric-services/assets/dashboard-kit/`** (FAA-17): `fabric-dashboard.css` (the workbench token
65
+ layer plus `--edge`; root `min-width: 360px`; fluid grids; navigation that wraps, or folds into a More menu
66
+ with the script; `.fd-scroll` boxes with edge shades, a visible scrollbar and keyboard focus; sticky chrome
67
+ kept within min(120 px, a quarter of the height) by letting the header scroll away), `fabric-dashboard.js`
68
+ (applies the `fabric_appearance` cookie, hides the page's own appearance controls while the host owns
69
+ them, re-applies on cookie change and visibility, folds navigation, names scroll boxes, measures the chrome
70
+ for anchors; CSP-clean), seven reference themes in `themes.json` (graphite, fabric, paper, forest, ocean,
71
+ ember, contrast; light and dark; every pair WCAG 2.2 AA, measured by `test/test_dashboard_kit.py`),
72
+ `kit.json` (version, theme ids, file digests and per-version history) and `demo.html`.
73
+ - **`scripts/dashboard_kit.py`** `status | install | build | notice`: `install` never overwrites a copy the
74
+ service changed without `--force`; `build` regenerates the themes from `themes.json`.
75
+ - **`scripts/check_dashboard_layout.mjs`**: renders a page in WebKit (or `--browser chromium|firefox`) at the
76
+ floor minus 40 and 360, 480, 768, 1280 px and reports ED-1…ED-5; `--service <id>` signs in with a login
77
+ code and reads the declared `minWidth`. Without Playwright every rule is `NOT_RUN`. It measures a settled
78
+ page: after the load it waits for the page's own requests to finish, and at each width for two frames,
79
+ 250 ms and two more frames, so a header that grows when its data arrives is not measured between its new
80
+ size and the kit's reaction to it (seen once on the brand board at 360 px: a false ED-5).
81
+ - **`check_service.py` rule `dashboard.declared`** (ED-9): PASS for a valid declaration (and names a newer
82
+ kit), FAIL for an invalid one, NOT_RUN for no dashboard or a legacy one.
83
+ - **`dashboard_extension()` / `dashboardExtension()`** in both kits, and `extensions` in `build_well_known` /
84
+ `buildWellKnown` (a dashboard declaration in it is validated). The sample service's dashboard is built on
85
+ the kit and declares it.
86
+ - **The plugin's first hook**: `SessionStart` runs `dashboard_kit.py notice`, which prints one line in a
87
+ repository whose dashboard has no kit or an older one and nothing elsewhere; it writes nothing and always
88
+ exits 0. `AGENTS.md` Lifecycle (LC-09) declares it.
89
+ - **`references/dashboard-kit.md`**: the rules, the markup, the themes, the declaration, the probes and
90
+ "Upgrading an existing dashboard" (only with the operator's go-ahead). Scenario evals
91
+ `dashboard-in-a-narrow-host-pane`, `dashboard-kit-upgrade-notice`, `dashboard-follows-host-appearance`
92
+ and two trigger queries came first.
93
+
94
+ ### Changed
95
+
96
+ - **Default contract pin `52da526` → `a83bec6b47776af688402c18efec507143061f68`.** `52da526` (DEC-0035) joins
97
+ `SUPPORTED_CONTRACT_COMMITS`. The revisions between add only the `fabric-dashboard/0.1` profile and
98
+ `service-dashboard-extension.schema.json`; the provider manifest and the MCP, A2A and local-runner
99
+ profiles do not change, and every descriptor and well-known document the kits wrote before stays valid.
100
+ - `references/dashboard.md` point 8 sends the page to the kit; `references/protocol.md` names the
101
+ `extensions` dashboard declaration.
102
+
3
103
  ## 0.8.3 - 2026-10-10
4
104
 
5
105
  The contract pin moves to Fabric Agent Contract `main` `52da526` (DEC-0034 and DEC-0035, the operator
package/README.md CHANGED
@@ -33,6 +33,17 @@ complete `scripts/sample_service.py`, and `scripts/check_service.py`, a live pro
33
33
  python3 plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py example-agent
34
34
  ```
35
35
 
36
+ The dashboard itself is built from the **dashboard kit** (`assets/dashboard-kit/`, `fabric-dashboard/0.1`,
37
+ contract DEC-0036): one stylesheet and one script that keep the page usable from 360 CSS px inside a host
38
+ pane beside the agent console — navigation that folds into a More menu, wide tables in their own scroll
39
+ box, sticky chrome at most 120 px — with seven WCAG AA themes (light and dark) that follow the host's
40
+ `fabric_appearance` cookie. `scripts/dashboard_kit.py install|status` copies and checks it,
41
+ `scripts/check_dashboard_layout.mjs` measures a rendered page in WebKit at 360, 480, 768 and 1280 px
42
+ (Playwright; `NOT_RUN` without it), and the probe's `dashboard.declared` checks the declaration. The
43
+ plugin's one hook (SessionStart) prints a single line in a repository whose dashboard has no kit or an
44
+ older one, and nothing anywhere else; the [kit reference](plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md)
45
+ has the upgrade steps, taken only with the operator's go-ahead.
46
+
36
47
  Dashboard handoffs use the host-generated `open_link` as the primary action. The
37
48
  three skills carry the same [consumer procedure](plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md)
38
49
  and a portable `scripts/check_dashboard_link.py` gate for generated renderers.
@@ -191,7 +202,7 @@ Scaffolding is non-destructive by default. It writes only the paths declared in
191
202
  The default pin for new bundles is [`fabric-contract.lock.json`](fabric-contract.lock.json):
192
203
 
193
204
  - version: `0.1.0`
194
- - commit: `52da526d7cc1e063d74c5b520dbc03d0ace15246`
205
+ - commit: `8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5`
195
206
 
196
207
  Every other mention of the default contract revision — this section, the skills' metadata, the
197
208
  skill card, `adapt_project.py` — must equal it; `python3 test/validate.py` fails a tree
@@ -218,8 +229,8 @@ FABRIC_CONTRACT_OLD=/path/to/legacy-contract FABRIC_CONTRACT_NEW=/path/to/curren
218
229
  ```
219
230
 
220
231
  Without both environment variables, those tests explicitly skip as `NOT_RUN`.
221
- This source checkout is 0.8.3. 0.8.2 is the latest published GitHub/npm release (2026-10-10; 0.7.0 was never
222
- released); 0.8.3 is published by its `v0.8.3` tag. Source delivery does not update installed skills or live sessions.
232
+ This source checkout is 0.10.0. 0.9.0 is the latest published GitHub/npm release (2026-10-11; 0.7.0 was never
233
+ released); 0.10.0 is published by its `v0.10.0` tag. Source delivery does not update installed skills or live sessions.
223
234
 
224
235
  ## License
225
236
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/fabric-agent-adapter",
3
- "version": "0.8.3",
3
+ "version": "0.10.0",
4
4
  "description": "Create Fabric-compatible agents, run them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract. This package is the installer CLI.",
5
5
  "scripts": {
6
6
  "test": "python3 test/validate.py && python3 -m unittest discover -s test -q && node --test 'test/*.test.mjs'"
@@ -3,7 +3,7 @@
3
3
  "name": "fabric-agent-adapter",
4
4
  "displayName": "Fabric Agent Adapter",
5
5
  "description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles.",
6
- "version": "0.8.3",
6
+ "version": "0.10.0",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -0,0 +1,17 @@
1
+ {
2
+ "description": "One line at session start in a repository whose Fabric dashboard has no dashboard kit or an older one (fabric-dashboard/0.1, DEC-0036). Silent everywhere else; reads only, never writes, always exits 0.",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "matcher": "startup|resume",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "python3 -S \"${CLAUDE_PLUGIN_ROOT}/skills/building-fabric-services/scripts/dashboard_kit.py\" notice",
11
+ "timeout": 5
12
+ }
13
+ ]
14
+ }
15
+ ]
16
+ }
17
+ }
@@ -5,9 +5,9 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
5
5
  compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
6
6
  metadata:
7
7
  author: PassionCode.ai
8
- version: "0.8.3"
8
+ version: "0.10.0"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "52da526d7cc1e063d74c5b520dbc03d0ace15246"
10
+ contract-commit: "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5"
11
11
  ---
12
12
 
13
13
  # Adapting projects to Fabric
@@ -70,7 +70,7 @@ Use exactly:
70
70
 
71
71
  - contract version `0.1.0`;
72
72
  - repository `https://github.com/passioncode-ai/fabric-agent-contract`;
73
- - commit `52da526d7cc1e063d74c5b520dbc03d0ace15246`.
73
+ - commit `8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5`.
74
74
 
75
75
  This is the default for new bundles. When checking an issued legacy bundle, preserve its
76
76
  explicit supported lock and use that exact clean checkout. Never silently upgrade or
@@ -16,10 +16,10 @@ from urllib.parse import urlparse
16
16
 
17
17
  CONTRACT_VERSION = "0.1.0"
18
18
  CONTRACT_REPOSITORY = "https://github.com/passioncode-ai/fabric-agent-contract"
19
- CONTRACT_COMMIT = "52da526d7cc1e063d74c5b520dbc03d0ace15246"
19
+ CONTRACT_COMMIT = "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5"
20
20
  # #region supported-contract-revisions — docs: README.md#contract-pin
21
21
  # Only this declaration may name a legacy revision in live files.
22
- SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
22
+ SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
23
23
  # #endregion supported-contract-revisions
24
24
  INTEROP_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
25
25
  MCP_REVISION = "2026-07-28"
@@ -9,17 +9,17 @@ description: >-
9
9
  на модели». Covers
10
10
  fabric-service/0.1: surfaces, token and one-time login, state, one copy, launchd, descriptor,
11
11
  well-known document, events, the usage report (spend per model); online: the https guard, the token-gated well-known document, the
12
- __Host- cookie, registering it on the operator's computer; ships Python and Node kits, a TLS
13
- sample and a live probe. NOT for a one-off script or cron job, a hosted product with no agent
12
+ __Host- cookie, registering it on the operator's computer; Python, Node and dashboard
13
+ kits, a TLS sample, live and layout probes. NOT for a one-off script or cron job, a hosted product with no agent
14
14
  behind it, the provider manifest (adapting-projects-to-fabric), or building Fabric Dashboards.
15
15
  license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
16
16
  compatibility: Python 3.9+ or Node.js 20+ for the kits; the probe needs Python 3.9+. launchd steps are macOS-only (Linux services use lifecycle manager none until a systemd adapter exists). Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
17
17
  metadata:
18
18
  author: PassionCode.ai
19
- version: "0.8.3"
19
+ version: "0.10.0"
20
20
  contract-version: "0.1.0"
21
21
  extension: "fabric-service/0.1"
22
- extension-commit: "52da526d7cc1e063d74c5b520dbc03d0ace15246"
22
+ extension-commit: "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5"
23
23
  ---
24
24
 
25
25
  # Building Fabric services
@@ -198,6 +198,17 @@ while the operator types or a dialog is open, one panel's failure never blanks t
198
198
  others, and every error is a sentence with the next action. Read
199
199
  [the dashboard reference](references/dashboard.md) before building the page.
200
200
 
201
+ The host shows the page in a pane beside the agent console that the operator can drag wide, so
202
+ the page must hold from **360 CSS px** up: no sideways document scroll, navigation that wraps or
203
+ folds into a More menu (never a tab strip that only scrolls sideways), wide tables and logs in
204
+ their own focusable scroll box, sticky chrome at most 120 px, and the host's colour scheme and
205
+ theme followed (`fabric-dashboard/0.1`, contract DEC-0036). **Build it from the dashboard kit**
206
+ (`assets/dashboard-kit/`: `dashboard_kit.py install <static dir>`, seven AA themes, light and dark)
207
+ and declare it with `extensions=fs.dashboard_extension()`. Read
208
+ [the dashboard kit reference](references/dashboard-kit.md) for the markup, the themes, the
209
+ declaration and how to upgrade a dashboard built before the kit — only with the operator's
210
+ go-ahead, which the plugin's session-start notice reminds you of.
211
+
201
212
  The events feed is a **view over the log you already keep** — a jobs table, a JSONL
202
213
  journal — not a second store. Each event is one sentence a person reads; set
203
214
  `notify: true` only when the operator must decide, or something failed or degrades their
@@ -224,10 +235,12 @@ python3 <skill-dir>/scripts/check_service.py <id>[.<instance>]
224
235
  It reports `PASS`, `FAIL` or `NOT_RUN` per rule (descriptor, port claim, well-known
225
236
  shape, identity, latency, Host/Origin/cross-site guards, loopback bind, token file,
226
237
  events auth and shape, single-use login, state outside code, instance lock, plist,
227
- launchd pid equals answering pid, and the `interop.*` rules: manifest link, capability
228
- list, tools equal to the manifest, job tools, unknown job, trace propagation, event
229
- trace pairs) and exits 1 on any `FAIL`. A `NOT_RUN` is not a pass —
230
- name it in the report.
238
+ launchd pid equals answering pid, `dashboard.declared` (ED-9), and the `interop.*` rules:
239
+ manifest link, capability list, tools equal to the manifest, job tools, unknown job, trace
240
+ propagation, event trace pairs) and exits 1 on any `FAIL`. A `NOT_RUN` is not a pass —
241
+ name it in the report. A service with a dashboard also runs the layout probe,
242
+ `node <skill-dir>/scripts/check_dashboard_layout.mjs --service <id>` (WebKit at 360, 480, 768
243
+ and 1280 px; needs Playwright, else `NOT_RUN`).
231
244
 
232
245
  Then prove the lock: start a second copy by hand against the same data directory; it
233
246
  must exit 75 while the first keeps serving. Prove recovery: `kill -9` the launchd pid;
@@ -0,0 +1,110 @@
1
+ <!doctype html>
2
+ <html lang="en" data-fd-appearance="scheme theme">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:">
7
+ <title>Example agent · dashboard kit demo</title>
8
+ <link rel="stylesheet" href="fabric-dashboard.css">
9
+ <script src="fabric-dashboard.js" defer></script>
10
+ </head>
11
+ <body>
12
+ <a class="fd-skip" href="#overview">Skip to content</a>
13
+ <div class="fd-chrome">
14
+ <header class="fd-header">
15
+ <div>
16
+ <p class="fd-title">Example agent</p>
17
+ <span class="fd-chip fd-s-ok"><span class="fd-dot"></span>Ready</span>
18
+ </div>
19
+ <div class="fd-actions">
20
+ <label class="fd-sr-only" for="theme">Theme</label>
21
+ <select id="theme" class="fd-select" data-fd-theme-picker></select>
22
+ <label class="fd-sr-only" for="scheme">Colour scheme</label>
23
+ <select id="scheme" class="fd-select" data-fd-scheme-picker></select>
24
+ <button class="fd-btn fd-btn--primary" type="button">Run now</button>
25
+ </div>
26
+ </header>
27
+ <nav class="fd-nav" aria-label="Sections">
28
+ <ul class="fd-nav-list">
29
+ <li><a href="#overview" aria-current="page">Overview</a></li>
30
+ <li><a href="#jobs">Jobs</a></li>
31
+ <li><a href="#approvals">Approvals</a></li>
32
+ <li><a href="#calendar">Calendar</a></li>
33
+ <li><a href="#channels">Channels</a></li>
34
+ <li><a href="#budget">Budget</a></li>
35
+ <li><a href="#journal">Journal</a></li>
36
+ <li><a href="#inbox">Inbox</a></li>
37
+ <li><a href="#cases">Cases</a></li>
38
+ <li><a href="#playbooks">Playbooks</a></li>
39
+ <li><a href="#reports">Reports</a></li>
40
+ <li><a href="#log">Log</a></li>
41
+ <li><a href="#settings">Settings</a></li>
42
+ <li><a href="#help">Help</a></li>
43
+ </ul>
44
+ </nav>
45
+ </div>
46
+
47
+ <main class="fd-page">
48
+ <section id="overview">
49
+ <h2>Overview</h2>
50
+ <div class="fd-grid fd-grid--tiles">
51
+ <div class="fd-tile"><div class="fd-label">Jobs today</div><div class="fd-value">128</div></div>
52
+ <div class="fd-tile"><div class="fd-label">Waiting for you</div><div class="fd-value">3</div></div>
53
+ <div class="fd-tile"><div class="fd-label">Spend, 7 days</div><div class="fd-value">$4.21</div></div>
54
+ <div class="fd-tile"><div class="fd-label">Last run</div><div class="fd-value"><time>17:02</time></div></div>
55
+ </div>
56
+ </section>
57
+
58
+ <section id="jobs">
59
+ <h2>Jobs</h2>
60
+ <div class="fd-scroll" data-fd-label="Jobs table">
61
+ <table class="fd-table">
62
+ <caption class="fd-sr-only">Recent jobs</caption>
63
+ <thead><tr><th>Job</th><th>Capability</th><th>State</th><th>Started</th><th class="fd-num">Tokens</th><th class="fd-num">Cost</th><th>Trace</th></tr></thead>
64
+ <tbody>
65
+ <tr><td class="fd-mono">job-7f3a91</td><td>example.summarize</td><td><span class="fd-chip fd-s-info"><span class="fd-dot"></span>Running</span></td><td><time>2026-10-11 17:02</time></td><td class="fd-num">12 480</td><td class="fd-num">$0.031</td><td class="fd-mono">4bf92f3577b34da6a3ce929d0e0e4736</td></tr>
66
+ <tr><td class="fd-mono">job-7f3a90</td><td>example.translate</td><td><span class="fd-chip fd-s-ok"><span class="fd-dot"></span>Done</span></td><td><time>2026-10-11 16:40</time></td><td class="fd-num">8 002</td><td class="fd-num">$0.019</td><td class="fd-mono">00f067aa0ba902b74bf92f3577b34da6</td></tr>
67
+ <tr><td class="fd-mono">job-7f3a8f</td><td>example.review</td><td><span class="fd-chip fd-s-warn"><span class="fd-dot"></span>Needs you</span></td><td><time>2026-10-11 15:12</time></td><td class="fd-num">2 210</td><td class="fd-num">—</td><td class="fd-mono">a3ce929d0e0e47364bf92f3577b34da6</td></tr>
68
+ </tbody>
69
+ </table>
70
+ </div>
71
+ </section>
72
+
73
+ <section id="approvals">
74
+ <h2>Approvals</h2>
75
+ <div class="fd-grid">
76
+ <article class="fd-card fd-stack">
77
+ <h3>Publish the weekly report <span class="fd-chip fd-s-warn"><span class="fd-dot"></span>Waiting</span></h3>
78
+ <p class="fd-hint">Before → after and the hash the service checks when it applies the change.</p>
79
+ <pre class="fd-scroll" data-fd-label="Change preview">- status: draft
80
+ + status: published sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08</pre>
81
+ <div class="fd-row"><button class="fd-btn fd-btn--primary" type="button">Approve</button><button class="fd-btn" type="button">Reject</button></div>
82
+ </article>
83
+ <article class="fd-card">
84
+ <h3>Service</h3>
85
+ <dl class="fd-pairs">
86
+ <div><dt>Build</dt><dd class="fd-mono">3537587</dd></div>
87
+ <div><dt>Started</dt><dd><time>2026-10-11 09:00</time></dd></div>
88
+ <div><dt>Port</dt><dd class="fd-mono">8710</dd></div>
89
+ <div><dt>Instance</dt><dd class="fd-mono">default</dd></div>
90
+ </dl>
91
+ </article>
92
+ </div>
93
+ </section>
94
+
95
+ <section id="calendar"><h2>Calendar</h2><p class="fd-empty">Nothing is scheduled this week. <a href="#jobs">See the jobs</a></p></section>
96
+ <section id="channels"><h2>Channels</h2><div class="fd-banner fd-s-danger"><span class="fd-dot fd-s-danger"></span><span>Channel push failed: the provider rejected the key. Settings → Keys</span></div></section>
97
+ <section id="budget"><h2>Budget</h2><p>$4.21 of $100 this month.</p></section>
98
+ <section id="journal"><h2>Journal</h2><p>No decisions recorded today.</p></section>
99
+ <section id="inbox"><h2>Inbox</h2><p class="fd-empty">No comments waiting.</p></section>
100
+ <section id="cases"><h2>Cases</h2><p>Two candidates from yesterday's work log.</p></section>
101
+ <section id="playbooks"><h2>Playbooks</h2><p>All playbooks reviewed this month.</p></section>
102
+ <section id="reports"><h2>Reports</h2><p>The weekly report is due Monday.</p></section>
103
+ <section id="log"><h2>Log</h2><pre class="fd-scroll" data-fd-label="Service log">2026-10-11T17:02:11Z info job.started job-7f3a91 example.summarize trace=4bf92f3577b34da6a3ce929d0e0e4736 span=00f067aa0ba902b7
104
+ 2026-10-11T16:40:57Z info job.finished job-7f3a90 example.translate tokens=8002 cost=0.019</pre></section>
105
+ <section id="settings"><h2>Settings</h2><label for="name">Display name</label> <input id="name" class="fd-input" value="Example agent"></section>
106
+ <section id="help"><h2>Help</h2><p>Every error is a sentence and the next action.</p></section>
107
+ </main>
108
+ <footer class="fd-foot">fabric-dashboard-kit demo · fabric-dashboard/0.1</footer>
109
+ </body>
110
+ </html>
@@ -0,0 +1,198 @@
1
+ /* fabric-dashboard-kit@1.0.0 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
2
+ A Fabric service dashboard shown inside a host pane: usable from 360 CSS px up, navigation always
3
+ reachable, wide content in its own scroll box, one vertical scroller, appearance from the host.
4
+ Copy this file next to fabric-dashboard.js; do not edit it in place. Put the page's own rules in
5
+ its own stylesheet, consuming var(--…) only. Themes are generated from themes.json by
6
+ `dashboard_kit.py build`; the block between the markers is overwritten. */
7
+
8
+ :root {
9
+ /* The floor (ED-1, ED-2) and the sticky chrome budget (ED-5). */
10
+ --fd-floor: 360px;
11
+ --fd-chrome-max: 120px;
12
+ --fd-chrome-h: 0px;
13
+ --fd-col: 320px;
14
+ --fd-page-max: 1440px;
15
+ --fd-gutter: clamp(12px, 3vw, 24px);
16
+
17
+ --r-control: 6px;
18
+ --r-card: 10px;
19
+ --r-pill: 999px;
20
+ --shadow-1: 0 4px 16px -8px rgba(26, 31, 43, 0.18);
21
+ --motion-ease: cubic-bezier(0.2, 0, 0, 1);
22
+ --dur-state: 0.18s;
23
+ --dur-hover: 0.12s;
24
+ --font-ui: -apple-system, "SF Pro", "Segoe UI", system-ui, sans-serif;
25
+ --font-data: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
26
+ --t-chip: 11px;
27
+ --t-label: 12px;
28
+ --t-body: 13px;
29
+ --t-card: 15px;
30
+ --t-section: 20px;
31
+ --t-page: 28px;
32
+ --space-1: 4px;
33
+ --space-2: 8px;
34
+ --space-3: 12px;
35
+ --space-4: 16px;
36
+ --space-5: 24px;
37
+ --space-6: 32px;
38
+ }
39
+
40
+ /* fabric-dashboard-kit:themes:begin */
41
+ :root, :root[data-theme="graphite"] { color-scheme: light; --bg: #f7f8fa; --panel: #ffffff; --panel-2: #f7f8fa; --ink: #1a1f2b; --muted: #5b6472; --border: #e6e9ef; --border-strong: #d7dce4; --edge: #828b98; --accent: #2f6feb; --accent-weak: #eaf0fe; --accent-ink: #ffffff; --ok: #1a7f37; --ok-weak: #e6f4ea; --warn: #9a6700; --warn-weak: #fff3d6; --danger: #d1242f; --danger-weak: #fde8e9; --info: #2f6feb; --info-weak: #eaf0fe; }
42
+ :root[data-scheme="dark"], :root[data-theme="graphite"][data-scheme="dark"] { color-scheme: dark; --bg: #0f1218; --panel: #161b24; --panel-2: #1b212c; --ink: #e8ecf3; --muted: #8a93a6; --border: #232a36; --border-strong: #2c3441; --edge: #697383; --accent: #4b8bff; --accent-weak: #1b2740; --accent-ink: #0f1218; --ok: #3fb960; --ok-weak: #12281a; --warn: #d9a93f; --warn-weak: #2b2210; --danger: #e5534b; --danger-weak: #2d1517; --info: #4b8bff; --info-weak: #1b2740; }
43
+ :root[data-theme="fabric"] { color-scheme: light; --bg: #f7f5f8; --panel: #ffffff; --panel-2: #f2eef4; --ink: #211725; --muted: #655769; --border: #e9e3ec; --border-strong: #d9d0de; --edge: #8f8296; --accent: #6b2185; --accent-weak: #f4e9f7; --accent-ink: #ffffff; --ok: #246b3b; --ok-weak: #e4f3e8; --warn: #8a4b0b; --warn-weak: #fff0dc; --danger: #a82f44; --danger-weak: #fde8ec; --info: #345da7; --info-weak: #e8efff; }
44
+ :root[data-theme="fabric"][data-scheme="dark"] { color-scheme: dark; --bg: #110c15; --panel: #1b1420; --panel-2: #231a29; --ink: #fff9f0; --muted: #b6aabc; --border: #2e2334; --border-strong: #3a2e41; --edge: #7a6a80; --accent: #d4a3ea; --accent-weak: #2f1d38; --accent-ink: #110c15; --ok: #79d49a; --ok-weak: #12291c; --warn: #f6ba75; --warn-weak: #332314; --danger: #ff969f; --danger-weak: #35191f; --info: #a6bfff; --info-weak: #18243d; }
45
+ :root[data-theme="paper"] { color-scheme: light; --bg: #faf6ee; --panel: #fffdf8; --panel-2: #f3ede1; --ink: #2b241a; --muted: #6b5f4d; --border: #ebe3d4; --border-strong: #ddd2bf; --edge: #8d806d; --accent: #8a4a12; --accent-weak: #f6eadb; --accent-ink: #ffffff; --ok: #2f6b2f; --ok-weak: #e7f1e2; --warn: #8a5a00; --warn-weak: #fbefd2; --danger: #b02a20; --danger-weak: #fbe5e1; --info: #2c5d8a; --info-weak: #e5eef6; }
46
+ :root[data-theme="paper"][data-scheme="dark"] { color-scheme: dark; --bg: #17140f; --panel: #1f1b15; --panel-2: #27221a; --ink: #f3ece0; --muted: #b3a690; --border: #302a21; --border-strong: #3d362b; --edge: #7b7161; --accent: #e3a565; --accent-weak: #33261a; --accent-ink: #17140f; --ok: #86c48a; --ok-weak: #15261a; --warn: #e0b25a; --warn-weak: #2e2412; --danger: #f0877c; --danger-weak: #331a17; --info: #8fb8e0; --info-weak: #172433; }
47
+ :root[data-theme="forest"] { color-scheme: light; --bg: #f4f8f4; --panel: #ffffff; --panel-2: #edf3ee; --ink: #16231a; --muted: #526356; --border: #dfe8e1; --border-strong: #cddbd0; --edge: #77897c; --accent: #1f7a45; --accent-weak: #e3f2e8; --accent-ink: #ffffff; --ok: #1f7a45; --ok-weak: #e3f2e8; --warn: #8a5d00; --warn-weak: #fbf0d4; --danger: #b8312a; --danger-weak: #fbe6e4; --info: #2a6496; --info-weak: #e4eef7; }
48
+ :root[data-theme="forest"][data-scheme="dark"] { color-scheme: dark; --bg: #0d1410; --panel: #131c16; --panel-2: #18231c; --ink: #e4efe7; --muted: #8fa595; --border: #1f2c23; --border-strong: #29382e; --edge: #637568; --accent: #5cc98a; --accent-weak: #173322; --accent-ink: #0d1410; --ok: #5cc98a; --ok-weak: #173322; --warn: #d8ad4a; --warn-weak: #2b2311; --danger: #ec6a5e; --danger-weak: #2f1714; --info: #7fb2e5; --info-weak: #152435; }
49
+ :root[data-theme="ocean"] { color-scheme: light; --bg: #f3f7f9; --panel: #ffffff; --panel-2: #ebf2f5; --ink: #132029; --muted: #4f6370; --border: #dde7ec; --border-strong: #ccdae1; --edge: #738892; --accent: #0b6e8a; --accent-weak: #e0f1f6; --accent-ink: #ffffff; --ok: #1b7a4a; --ok-weak: #e2f3ea; --warn: #8f5f00; --warn-weak: #fcf0d3; --danger: #c0352b; --danger-weak: #fce6e3; --info: #1f5fae; --info-weak: #e3edfa; }
50
+ :root[data-theme="ocean"][data-scheme="dark"] { color-scheme: dark; --bg: #0b1217; --panel: #111a21; --panel-2: #16212a; --ink: #e2edf3; --muted: #8aa2b1; --border: #1c2a34; --border-strong: #263741; --edge: #607480; --accent: #3fc0d8; --accent-weak: #123039; --accent-ink: #0b1217; --ok: #4fc58a; --ok-weak: #11291d; --warn: #dcac45; --warn-weak: #2c2310; --danger: #ef6b60; --danger-weak: #311714; --info: #6fa8f0; --info-weak: #13233a; }
51
+ :root[data-theme="ember"] { color-scheme: light; --bg: #fbf6f2; --panel: #ffffff; --panel-2: #f6ede6; --ink: #2a1b14; --muted: #6e5649; --border: #efe2d8; --border-strong: #e2d1c4; --edge: #958072; --accent: #b04a0c; --accent-weak: #fbebe0; --accent-ink: #ffffff; --ok: #2a7340; --ok-weak: #e4f2e7; --warn: #8a5800; --warn-weak: #fbefd3; --danger: #b42324; --danger-weak: #fbe4e3; --info: #2b5f9e; --info-weak: #e4edf8; }
52
+ :root[data-theme="ember"][data-scheme="dark"] { color-scheme: dark; --bg: #16100c; --panel: #1e1611; --panel-2: #271d16; --ink: #f6ebe3; --muted: #b8a194; --border: #30241c; --border-strong: #3e2f25; --edge: #7e6a5e; --accent: #ff8f4d; --accent-weak: #3a2112; --accent-ink: #16100c; --ok: #78c88e; --ok-weak: #132719; --warn: #e3b04f; --warn-weak: #2e2410; --danger: #f4776f; --danger-weak: #351816; --info: #86b2ef; --info-weak: #162338; }
53
+ :root[data-theme="contrast"] { color-scheme: light; --bg: #ffffff; --panel: #ffffff; --panel-2: #f2f2f2; --ink: #000000; --muted: #2b2b2b; --border: #000000; --border-strong: #000000; --edge: #000000; --accent: #0033cc; --accent-weak: #e6ecff; --accent-ink: #ffffff; --ok: #005c1f; --ok-weak: #e3f5e8; --warn: #6b4200; --warn-weak: #fff1d1; --danger: #b00010; --danger-weak: #ffe6e8; --info: #0033cc; --info-weak: #e6ecff; }
54
+ :root[data-theme="contrast"][data-scheme="dark"] { color-scheme: dark; --bg: #000000; --panel: #000000; --panel-2: #141414; --ink: #ffffff; --muted: #e0e0e0; --border: #ffffff; --border-strong: #ffffff; --edge: #ffffff; --accent: #7fb2ff; --accent-weak: #0d1f3d; --accent-ink: #000000; --ok: #5ee08a; --ok-weak: #0a2614; --warn: #ffd166; --warn-weak: #2e2306; --danger: #ff8a8a; --danger-weak: #3a0d0d; --info: #7fb2ff; --info-weak: #0d1f3d; }
55
+ @media (prefers-color-scheme: dark) {
56
+ :root:not([data-scheme="light"]) { color-scheme: dark; --bg: #0f1218; --panel: #161b24; --panel-2: #1b212c; --ink: #e8ecf3; --muted: #8a93a6; --border: #232a36; --border-strong: #2c3441; --edge: #697383; --accent: #4b8bff; --accent-weak: #1b2740; --accent-ink: #0f1218; --ok: #3fb960; --ok-weak: #12281a; --warn: #d9a93f; --warn-weak: #2b2210; --danger: #e5534b; --danger-weak: #2d1517; --info: #4b8bff; --info-weak: #1b2740; }
57
+ :root[data-theme="fabric"]:not([data-scheme="light"]) { color-scheme: dark; --bg: #110c15; --panel: #1b1420; --panel-2: #231a29; --ink: #fff9f0; --muted: #b6aabc; --border: #2e2334; --border-strong: #3a2e41; --edge: #7a6a80; --accent: #d4a3ea; --accent-weak: #2f1d38; --accent-ink: #110c15; --ok: #79d49a; --ok-weak: #12291c; --warn: #f6ba75; --warn-weak: #332314; --danger: #ff969f; --danger-weak: #35191f; --info: #a6bfff; --info-weak: #18243d; }
58
+ :root[data-theme="paper"]:not([data-scheme="light"]) { color-scheme: dark; --bg: #17140f; --panel: #1f1b15; --panel-2: #27221a; --ink: #f3ece0; --muted: #b3a690; --border: #302a21; --border-strong: #3d362b; --edge: #7b7161; --accent: #e3a565; --accent-weak: #33261a; --accent-ink: #17140f; --ok: #86c48a; --ok-weak: #15261a; --warn: #e0b25a; --warn-weak: #2e2412; --danger: #f0877c; --danger-weak: #331a17; --info: #8fb8e0; --info-weak: #172433; }
59
+ :root[data-theme="forest"]:not([data-scheme="light"]) { color-scheme: dark; --bg: #0d1410; --panel: #131c16; --panel-2: #18231c; --ink: #e4efe7; --muted: #8fa595; --border: #1f2c23; --border-strong: #29382e; --edge: #637568; --accent: #5cc98a; --accent-weak: #173322; --accent-ink: #0d1410; --ok: #5cc98a; --ok-weak: #173322; --warn: #d8ad4a; --warn-weak: #2b2311; --danger: #ec6a5e; --danger-weak: #2f1714; --info: #7fb2e5; --info-weak: #152435; }
60
+ :root[data-theme="ocean"]:not([data-scheme="light"]) { color-scheme: dark; --bg: #0b1217; --panel: #111a21; --panel-2: #16212a; --ink: #e2edf3; --muted: #8aa2b1; --border: #1c2a34; --border-strong: #263741; --edge: #607480; --accent: #3fc0d8; --accent-weak: #123039; --accent-ink: #0b1217; --ok: #4fc58a; --ok-weak: #11291d; --warn: #dcac45; --warn-weak: #2c2310; --danger: #ef6b60; --danger-weak: #311714; --info: #6fa8f0; --info-weak: #13233a; }
61
+ :root[data-theme="ember"]:not([data-scheme="light"]) { color-scheme: dark; --bg: #16100c; --panel: #1e1611; --panel-2: #271d16; --ink: #f6ebe3; --muted: #b8a194; --border: #30241c; --border-strong: #3e2f25; --edge: #7e6a5e; --accent: #ff8f4d; --accent-weak: #3a2112; --accent-ink: #16100c; --ok: #78c88e; --ok-weak: #132719; --warn: #e3b04f; --warn-weak: #2e2410; --danger: #f4776f; --danger-weak: #351816; --info: #86b2ef; --info-weak: #162338; }
62
+ :root[data-theme="contrast"]:not([data-scheme="light"]) { color-scheme: dark; --bg: #000000; --panel: #000000; --panel-2: #141414; --ink: #ffffff; --muted: #e0e0e0; --border: #ffffff; --border-strong: #ffffff; --edge: #ffffff; --accent: #7fb2ff; --accent-weak: #0d1f3d; --accent-ink: #000000; --ok: #5ee08a; --ok-weak: #0a2614; --warn: #ffd166; --warn-weak: #2e2306; --danger: #ff8a8a; --danger-weak: #3a0d0d; --info: #7fb2ff; --info-weak: #0d1f3d; }
63
+ }
64
+ /* fabric-dashboard-kit:themes:end */
65
+
66
+ @media (prefers-reduced-motion: reduce) {
67
+ :root { --dur-state: 0s; --dur-hover: 0s; }
68
+ }
69
+
70
+ /* ---- base: the floor holds (ED-1, ED-2) ---- */
71
+ *, *::before, *::after { box-sizing: border-box; }
72
+ html { min-width: var(--fd-floor); background: var(--bg); color: var(--ink); -webkit-text-size-adjust: 100%; }
73
+ body { margin: 0; background: var(--bg); color: var(--ink); font: 400 var(--t-body)/1.5 var(--font-ui); overflow-wrap: break-word; }
74
+ img, svg, video, canvas { max-width: 100%; height: auto; }
75
+ h1, h2, h3 { margin: 0; text-wrap: balance; }
76
+ h1 { font-size: var(--t-section); font-weight: 700; }
77
+ h2 { font-size: var(--t-section); font-weight: 700; margin: var(--space-6) 0 var(--space-3); }
78
+ h3 { font-size: var(--t-card); font-weight: 600; margin: 0 0 var(--space-3); }
79
+ p { max-width: 65ch; }
80
+ a { color: var(--ink); text-decoration: underline; text-decoration-color: var(--edge); text-underline-offset: 2px; }
81
+ a:hover { text-decoration-color: var(--ink); }
82
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
83
+ /* A jump to an anchor lands below the sticky chrome (ED-5); the script measures --fd-chrome-h. */
84
+ [id] { scroll-margin-top: calc(var(--fd-chrome-h) + var(--space-2)); }
85
+ .fd-mono, .fd-num, time { font-family: var(--font-data); font-variant-numeric: tabular-nums; }
86
+ .fd-muted { color: var(--muted); }
87
+ .fd-hint { color: var(--muted); font-size: var(--t-label); max-width: 65ch; margin: 0 0 var(--space-3); }
88
+ .fd-break { overflow-wrap: anywhere; }
89
+ .fd-sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
90
+ .fd-skip { position: absolute; top: var(--space-2); left: var(--space-4); z-index: 20; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
91
+ .fd-skip:focus { width: auto; height: auto; clip: auto; overflow: visible; background: var(--panel); padding: var(--space-2); }
92
+
93
+ /* ---- page frame ---- */
94
+ .fd-page { max-width: var(--fd-page-max); margin: 0 auto; padding: var(--space-4) var(--fd-gutter) var(--space-6); }
95
+ /* Sticky only once the script runs: without it the navigation wraps and a wrapped bar must not pin
96
+ half the screen. The script keeps the pinned part at or under min(--fd-chrome-max, 25vh) (ED-5):
97
+ when header and navigation together are taller, the header scrolls away (a negative top) and the
98
+ navigation stays pinned. */
99
+ .fd-chrome { background: var(--panel); border-bottom: 1px solid var(--border); }
100
+ html.fd-js .fd-chrome { position: sticky; top: var(--fd-chrome-top, 0px); z-index: 10; }
101
+ .fd-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-4);
102
+ padding: var(--space-3) var(--fd-gutter); min-width: 0; }
103
+ .fd-header > * { min-width: 0; }
104
+ .fd-title { font-size: var(--t-card); font-weight: 700; margin: 0; }
105
+ .fd-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
106
+ .fd-foot { color: var(--muted); font-size: var(--t-label); padding: var(--space-4) var(--fd-gutter); border-top: 1px solid var(--border); }
107
+
108
+ /* ---- navigation: always reachable (ED-3) ----
109
+ Without the script the list wraps. With it (.fd-nav--fold) the list stays one line and the
110
+ links that do not fit move into the More menu; nothing is left past a hidden scrollbar. */
111
+ .fd-nav { display: flex; align-items: center; gap: var(--space-1); padding: var(--space-1) var(--fd-gutter); min-width: 0; }
112
+ .fd-nav-list { display: flex; flex-wrap: wrap; gap: var(--space-1); list-style: none; margin: 0; padding: 0; min-width: 0; }
113
+ .fd-nav--fold .fd-nav-list { flex: 1 1 auto; flex-wrap: nowrap; overflow: hidden; }
114
+ .fd-nav a, .fd-more > summary { display: block; padding: var(--space-1) var(--space-3); border-radius: var(--r-control); color: var(--muted);
115
+ text-decoration: none; white-space: nowrap; cursor: pointer;
116
+ transition: background-color var(--dur-hover) var(--motion-ease), color var(--dur-hover) var(--motion-ease); }
117
+ .fd-nav a:hover, .fd-more > summary:hover { background: var(--panel-2); color: var(--ink); }
118
+ .fd-nav a[aria-current] { background: var(--accent-weak); color: var(--ink); box-shadow: inset 0 -2px 0 var(--accent); }
119
+ .fd-more { position: relative; flex: none; }
120
+ .fd-more[hidden] { display: none; }
121
+ .fd-more > summary { list-style: none; border: 1px solid var(--edge); }
122
+ .fd-more > summary::-webkit-details-marker { display: none; }
123
+ .fd-more > summary::after { content: " \25BE"; }
124
+ .fd-more-list { position: absolute; right: 0; top: calc(100% + var(--space-1)); z-index: 30; list-style: none; margin: 0; padding: var(--space-1);
125
+ min-width: 12rem; max-width: min(20rem, calc(100vw - 2 * var(--fd-gutter))); max-height: 60vh; overflow-y: auto;
126
+ background: var(--panel); border: 1px solid var(--edge); border-radius: var(--r-card); box-shadow: var(--shadow-1); }
127
+ .fd-more-list a { white-space: normal; }
128
+
129
+ /* ---- layout: fluid, no fixed widths ---- */
130
+ .fd-grid { display: grid; gap: var(--space-3); grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--fd-col)), 1fr)); }
131
+ .fd-grid--tiles { --fd-col: 160px; }
132
+ .fd-grid--wide { --fd-col: 480px; }
133
+ .fd-stack > * + * { margin-top: var(--space-3); }
134
+ .fd-row { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); align-items: center; min-width: 0; }
135
+ .fd-row > * { min-width: 0; }
136
+ /* A label-value list: two columns while they fit, one below that. */
137
+ .fd-pairs { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); gap: var(--space-1) var(--space-4); margin: 0; }
138
+ .fd-pairs > div { display: flex; justify-content: space-between; gap: var(--space-3); border-bottom: 1px solid var(--border); padding: var(--space-1) 0; min-width: 0; }
139
+ .fd-pairs dt { color: var(--muted); }
140
+ .fd-pairs dd { margin: 0; text-align: right; overflow-wrap: anywhere; }
141
+
142
+ /* ---- wide content scrolls in its own box (ED-4) ----
143
+ Edge shades show which side has more (the local/scroll background pair), the WebKit scrollbar
144
+ stays visible instead of overlaying, and the script makes the box focusable with a name. */
145
+ .fd-scroll { overflow-x: auto; max-width: 100%; border: 1px solid var(--border); border-radius: var(--r-card);
146
+ background: var(--panel); /* a browser without color-mix() keeps a plain box */
147
+ background:
148
+ linear-gradient(90deg, var(--panel) 30%, transparent) left center / 40px 100% no-repeat local,
149
+ linear-gradient(270deg, var(--panel) 30%, transparent) right center / 40px 100% no-repeat local,
150
+ linear-gradient(90deg, color-mix(in srgb, var(--ink) 18%, transparent), transparent) left center / 14px 100% no-repeat scroll,
151
+ linear-gradient(270deg, color-mix(in srgb, var(--ink) 18%, transparent), transparent) right center / 14px 100% no-repeat scroll,
152
+ var(--panel);
153
+ scrollbar-width: thin; scrollbar-color: var(--edge) transparent; }
154
+ .fd-scroll::-webkit-scrollbar { height: 10px; width: 10px; }
155
+ .fd-scroll::-webkit-scrollbar-thumb { background: var(--edge); border-radius: var(--r-pill); border: 2px solid var(--panel); }
156
+ .fd-scroll::-webkit-scrollbar-track { background: transparent; }
157
+ .fd-scroll:focus-visible { outline-offset: -2px; }
158
+ .fd-scroll[data-fd-overflow] { border-color: var(--edge); }
159
+ pre.fd-scroll, .fd-scroll pre { margin: 0; padding: var(--space-3); font: var(--t-body)/1.5 var(--font-data); white-space: pre; }
160
+ .fd-table { border-collapse: collapse; width: 100%; font-variant-numeric: tabular-nums; }
161
+ /* Inside its own box a table keeps its natural width and the box scrolls; words are not broken. */
162
+ .fd-scroll > .fd-table { width: max-content; min-width: 100%; }
163
+ .fd-scroll .fd-table td, .fd-scroll .fd-table th { overflow-wrap: normal; }
164
+ .fd-table th, .fd-table td { text-align: left; padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--border); vertical-align: top; }
165
+ .fd-table th { background: var(--panel-2); color: var(--muted); font-size: var(--t-label); font-weight: 600; white-space: nowrap; }
166
+ .fd-table tbody tr:hover { background: var(--panel-2); }
167
+ .fd-table td.fd-num, .fd-table th.fd-num { text-align: right; }
168
+
169
+ /* ---- atoms ---- */
170
+ .fd-card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-card); padding: var(--space-4); min-width: 0; }
171
+ .fd-card > h3:first-child { display: flex; justify-content: space-between; gap: var(--space-2); flex-wrap: wrap; }
172
+ .fd-tile { background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-card); padding: var(--space-3); min-width: 0; }
173
+ .fd-tile .fd-label { color: var(--muted); font-size: var(--t-label); }
174
+ .fd-tile .fd-value { font: 600 var(--t-section)/1.2 var(--font-data); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
175
+ /* Status: the word in --ink, the colour in the dot and the fill; never colour alone. */
176
+ .fd-chip { display: inline-flex; align-items: center; gap: var(--space-1); max-width: 100%; font: var(--t-chip)/1.6 var(--font-data);
177
+ padding: 0 var(--space-2); border: 1px solid var(--border-strong); border-radius: var(--r-pill); color: var(--ink); background: var(--panel);
178
+ white-space: nowrap; }
179
+ .fd-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; }
180
+ .fd-s-ok { background: var(--ok-weak); } .fd-s-ok .fd-dot, .fd-dot.fd-s-ok { background: var(--ok); }
181
+ .fd-s-warn { background: var(--warn-weak); } .fd-s-warn .fd-dot, .fd-dot.fd-s-warn { background: var(--warn); }
182
+ .fd-s-danger { background: var(--danger-weak); } .fd-s-danger .fd-dot, .fd-dot.fd-s-danger { background: var(--danger); }
183
+ .fd-s-info { background: var(--info-weak); } .fd-s-info .fd-dot, .fd-dot.fd-s-info { background: var(--info); }
184
+ .fd-banner { display: flex; gap: var(--space-2); align-items: flex-start; padding: var(--space-3); border: 1px solid var(--edge); border-radius: var(--r-card); }
185
+ .fd-btn { font: 600 var(--t-body) var(--font-ui); padding: var(--space-2) var(--space-3); border-radius: var(--r-control); cursor: pointer;
186
+ border: 1px solid var(--edge); background: var(--panel); color: var(--ink); max-width: 100%;
187
+ transition: background-color var(--dur-hover) var(--motion-ease), border-color var(--dur-hover) var(--motion-ease); }
188
+ .fd-btn:hover { background: var(--panel-2); }
189
+ .fd-btn--primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
190
+ .fd-btn--primary:hover { background: var(--accent); filter: brightness(1.06); }
191
+ .fd-btn--danger { border-color: var(--danger); }
192
+ .fd-btn:disabled { cursor: not-allowed; opacity: 0.55; }
193
+ .fd-input, .fd-select, .fd-textarea { font: var(--t-body) var(--font-ui); color: var(--ink); background: var(--panel);
194
+ border: 1px solid var(--edge); border-radius: var(--r-control); padding: var(--space-2); max-width: 100%; min-width: 0; }
195
+ .fd-empty { color: var(--muted); padding: var(--space-4); text-align: center; }
196
+
197
+ /* ---- appearance owned by the host (ED-6) ---- */
198
+ html[data-fd-host-appearance] [data-fd-appearance-control] { display: none !important; }