@passioncode-ai/passioncode 0.1.32 → 0.1.33

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 +12 -0
  2. package/family.json +1 -1
  3. package/package.json +1 -1
  4. package/payload/.claude-plugin/marketplace.json +2 -2
  5. package/payload/manifest.json +8 -8
  6. package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/payload/plugins/fabric-agent-adapter/hooks/hooks.json +17 -0
  8. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
  9. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  10. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +21 -8
  11. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/demo.html +110 -0
  12. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +198 -0
  13. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +253 -0
  14. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +28 -0
  15. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/themes.json +350 -0
  16. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +133 -0
  17. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +4 -3
  18. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +7 -2
  19. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +295 -0
  20. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +31 -0
  21. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +437 -0
  22. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +51 -1
  23. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +65 -2
  24. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +21 -3
  25. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
  26. package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.33 - 2026-10-11
4
+
5
+ ### Changed
6
+
7
+ - **Fabric Agent Adapter is pinned at `v0.9.0`** (it was `v0.8.3`). `building-fabric-services` ships the dashboard
8
+ kit for `fabric-dashboard/0.1` (Fabric Agent Contract DEC-0036): one stylesheet and one script that keep a
9
+ service's dashboard usable from 360 CSS px inside a host pane beside the agent console, seven WCAG AA themes in
10
+ light and dark that follow the host's `fabric_appearance` cookie, `dashboard_kit.py`, the layout probe
11
+ `check_dashboard_layout.mjs` and the probe rule `dashboard.declared`. The adapter plugin gains its first hook:
12
+ at session start it prints one line in a repository whose dashboard has no kit or an older one, and nothing
13
+ elsewhere. The default contract pin moves to `a83bec6`; bundles issued under the earlier revisions stay valid.
14
+
3
15
  ## 0.1.32 - 2026-10-10
4
16
 
5
17
  ### Changed
package/family.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "name": "fabric-agent-adapter",
9
9
  "displayName": "Fabric Agent Adapter",
10
10
  "repo": "passioncode-ai/fabric-agent-adapter",
11
- "ref": "v0.8.3",
11
+ "ref": "v0.9.0",
12
12
  "kind": "plugin",
13
13
  "path": "plugins/fabric-agent-adapter",
14
14
  "legacyPluginIds": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/passioncode",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "description": "The PassionCode.ai agent skill set \u2014 Fabric Agent Adapter, Observatory Log and the organisation's working rules \u2014 for Claude Code and every other agent on the machine, updated as one set.",
5
5
  "bin": {
6
6
  "passioncode": "bin/passioncode.js"
@@ -11,7 +11,7 @@
11
11
  "name": "fabric-agent-adapter",
12
12
  "displayName": "Fabric Agent Adapter",
13
13
  "source": "./plugins/fabric-agent-adapter",
14
- "version": "0.8.3",
14
+ "version": "0.9.0",
15
15
  "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.",
16
16
  "author": {
17
17
  "name": "PassionCode.ai",
@@ -35,7 +35,7 @@
35
35
  "name": "passioncode",
36
36
  "displayName": "PassionCode.ai",
37
37
  "source": "./plugins/passioncode",
38
- "version": "0.1.32",
38
+ "version": "0.1.33",
39
39
  "description": "The organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
40
40
  "author": {
41
41
  "name": "PassionCode.ai",
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "family": "passioncode",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "release": true,
5
5
  "members": [
6
6
  {
7
7
  "name": "fabric-agent-adapter",
8
8
  "displayName": "Fabric Agent Adapter",
9
9
  "repo": "passioncode-ai/fabric-agent-adapter",
10
- "ref": "v0.8.3",
11
- "commit": "0577591f173005dea5a150978271ab9180be9082",
12
- "version": "0.8.3",
10
+ "ref": "v0.9.0",
11
+ "commit": "7e45e6be5f17e0a638acf41e3a6b499aa410127a",
12
+ "version": "0.9.0",
13
13
  "via": "clone of https://***@github.com/passioncode-ai/fabric-agent-adapter.git",
14
14
  "skills": [
15
15
  "adapting-projects-to-fabric",
@@ -22,7 +22,7 @@
22
22
  "legacyMarketplaces": [
23
23
  "fabric-agent-adapter"
24
24
  ],
25
- "contentHash": "sha256:63fb97a3ae1696044e6761904b649fb73eab9c7d6659340271e98eb681c51eff",
25
+ "contentHash": "sha256:991de28ab45fdcf9ee364ccbaefa9e7b147608b5928aae5d239e478244e078f4",
26
26
  "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.",
27
27
  "author": {
28
28
  "name": "PassionCode.ai",
@@ -61,16 +61,16 @@
61
61
  "name": "passioncode",
62
62
  "displayName": "PassionCode.ai",
63
63
  "repo": "passioncode-ai/passioncode",
64
- "ref": "v0.1.32",
64
+ "ref": "v0.1.33",
65
65
  "commit": null,
66
- "version": "0.1.32",
66
+ "version": "0.1.33",
67
67
  "via": "this repository",
68
68
  "skills": [
69
69
  "working-in-passioncode"
70
70
  ],
71
71
  "legacyPluginIds": [],
72
72
  "legacyMarketplaces": [],
73
- "contentHash": "sha256:1d967e26c679870c5af77ab293563e27e1980af00df7787750e95ef6694e39dd",
73
+ "contentHash": "sha256:3fed1a8d8775dc09826ff7e4edefb10a99827b97dd6cd0fe8345388b7e501a0d",
74
74
  "description": "The organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
75
75
  "author": {
76
76
  "name": "PassionCode.ai",
@@ -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.9.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.9.0"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "52da526d7cc1e063d74c5b520dbc03d0ace15246"
10
+ contract-commit: "a83bec6b47776af688402c18efec507143061f68"
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 `a83bec6b47776af688402c18efec507143061f68`.
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 = "a83bec6b47776af688402c18efec507143061f68"
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, "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.9.0"
20
20
  contract-version: "0.1.0"
21
21
  extension: "fabric-service/0.1"
22
- extension-commit: "52da526d7cc1e063d74c5b520dbc03d0ace15246"
22
+ extension-commit: "a83bec6b47776af688402c18efec507143061f68"
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; }