@jenga-ai/agent 3.4.0 → 3.6.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.
- package/README.md +85 -78
- package/agents/developer.md +1 -1
- package/agents/scrum-master.md +20 -2
- package/agents/tester.md +3 -3
- package/hooks/on_session_end.sh +5 -5
- package/lib/generate-agent-context.js +2 -2
- package/lib/generate-copilot-hooks.js +1 -1
- package/lib/generate-skill-allow-list.js +37 -3
- package/lib/mirror.js +1 -1
- package/lib/postinstall-manifest.js +1 -1
- package/lib/skill-allow-list.json +2 -2
- package/mcp/help/index.js +8 -17
- package/mcp/help/scan.js +73 -0
- package/package.json +6 -1
- package/project/app/api/parsers/knowledge-graph.js +100 -9
- package/project/app/api/routes/health.js +36 -0
- package/project/app/api/scripts/capture-snapshot.js +9 -6
- package/project/app/ui/dist/assets/{index-CdK3Qrep.css → index-BVR_7Owg.css} +1 -1
- package/project/app/ui/dist/assets/index-CtU2xLQm.js +104 -0
- package/project/app/ui/dist/index.html +2 -2
- package/project/app/ui/package.json +4 -0
- package/project/app/ui/scripts/build-snapshot-html.cjs +63 -2
- package/scripts/acquire-concurrency-slot.sh +1 -1
- package/scripts/apply-j-prefix.sh +46 -5
- package/scripts/audit-twin-divergence.sh +73 -5
- package/scripts/build-pages-site.sh +1 -1
- package/scripts/check-public-playbook-steps.sh +158 -52
- package/scripts/check-publicignore-match.sh +2 -2
- package/scripts/compute-deploy-reconcile.sh +5 -5
- package/scripts/delete-bare-skill-dirs.sh +330 -0
- package/scripts/generate-legacy-shipped-paths.js +2 -2
- package/scripts/idea_manager.sh +258 -3
- package/scripts/mark-deployed.sh +2 -2
- package/scripts/populate-knowledge-graph.entity-resolution.test.js +254 -0
- package/scripts/populate-knowledge-graph.js +213 -5
- package/scripts/populate-knowledge-graph.staleness.test.js +130 -0
- package/scripts/postinstall.js +1 -1
- package/scripts/repoint-skill-refs.sh +539 -0
- package/scripts/todo_manager.sh +1 -1
- package/scripts/verify-legacy-seed-reconcile.sh +10 -10
- package/scripts/verify-postinstall-reconcile.sh +7 -7
- package/scripts/write-context-digest.sh +1 -1
- package/skills/j-clearify/SKILL.md +2 -2
- package/skills/j-close-story/scripts/check-privatized.sh +4 -4
- package/skills/j-distribute/CONFIG_SCHEMA.md +82 -5
- package/skills/j-do/SKILL.md +101 -17
- package/skills/j-doc-sync/SKILL.md +1 -0
- package/skills/j-gitignore/SKILL.md +157 -0
- package/skills/j-gitignore/assets/jenga-paths.txt +50 -0
- package/skills/j-gitignore/scripts/_catalog.sh +105 -0
- package/skills/j-gitignore/scripts/audit-gitignore.sh +194 -0
- package/skills/j-gitignore/scripts/repair-gitignore.sh +226 -0
- package/skills/j-gitignore/scripts/untrack-jenga-files.sh +210 -0
- package/skills/j-idea/SKILL.md +78 -6
- package/skills/j-idea/assets/idea_template.md +1 -1
- package/skills/j-improve/SKILL.md +1 -1
- package/skills/j-init/SKILL.md +53 -14
- package/skills/j-init/assets/.gitignore_template +1 -2
- package/skills/j-init/assets/scope-thresholds_template.json +5 -2
- package/skills/j-init/scripts/apply-scaffold-visibility.sh +192 -0
- package/skills/j-init/scripts/init.sh +22 -8
- package/skills/j-playbook/SKILL.md +1 -1
- package/skills/j-publish/SKILL.md +1 -1
- package/skills/j-publish/adapters/npm-ci.md +6 -1
- package/skills/j-publish/adapters/npm.md +1 -1
- package/skills/j-publish/scripts/generate_release_notes.sh +1 -1
- package/skills/j-publish/scripts/npm_stage_inspect.sh +61 -0
- package/skills/j-reconcile/SKILL.md +2 -2
- package/skills/j-reconcile/scripts/detect-unlinked-code.sh +11 -11
- package/skills/j-redo/SKILL.md +1 -1
- package/skills/j-skillify/assets/init-new/assets/.gitignore_template +1 -2
- package/skills/j-spinoff/SKILL.md +1 -1
- package/skills/j-status/SKILL.md +15 -0
- package/skills/j-todo/SKILL.md +3 -1
- package/skills/j-uncharted/SKILL.md +55 -8
- package/skills/j-uncharted/assets/NODE_QUESTION_TEMPLATE.md +69 -0
- package/skills/j-uncharted/scripts/detect-dependencies.sh +1 -1
- package/skills/j-uncharted/scripts/detect-tests.sh +1 -1
- package/skills/j-uncharted/scripts/elicitation-state.sh +46 -8
- package/skills/j-uncharted/scripts/validate-proposed-items.sh +1 -1
- package/skills/j-wtf/SKILL.md +1 -1
- package/skills/jenga/SKILL.md +43 -9
- package/skills/jenga/playbooks/board-hygiene.json +32 -0
- package/skills/jenga/playbooks/schema.json +73 -6
- package/skills/jenga/playbooks/understand-then-commit.json +19 -0
- package/skills/jenga/scripts/load-nl-catalog.sh +1 -1
- package/skills/jenga/scripts/load-playbooks.sh +23 -11
- package/skills/jenga/scripts/match-playbook.sh +1 -1
- package/templates/SCRUM_BOARD_SCHEMA.md +14 -1
- package/templates/SKILL_TEMPLATE.md +12 -0
- package/templates/permission-levels/level-4-elevated.json +1 -1
- package/templates/permission-levels/level-5-unrestricted.json +1 -1
- package/templates/playbook-types.json +34 -6
- package/project/app/ui/dist/assets/index-7fj-vllY.js +0 -104
- package/scripts/generate-j-alias.sh +0 -333
- package/skills/j-dev-done/SKILL.md +0 -53
- package/skills/j-dev-done/scripts/classify-commit-outcome.sh +0 -114
|
@@ -37,11 +37,56 @@ function readJsonSafe(filePath) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// E20_S10_T01: nodes whose provenance is genuinely architectural — a human-confirmed elicitation
|
|
41
|
+
// (`source: 'human'`) or a mechanical AST-derived extraction (`source: 'ast'`), per
|
|
42
|
+
// STUB_SCHEMA.md's Provenance Field section.
|
|
43
|
+
const VERIFIED_SOURCES = new Set(['human', 'ast']);
|
|
44
|
+
|
|
45
|
+
function isVerifiedSource(node) {
|
|
46
|
+
return Boolean(node) && VERIFIED_SOURCES.has(node.source);
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
/**
|
|
41
50
|
* Read and transform `project/knowledge-graph/graph.json` into the UI-facing SAD map shape.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
51
|
+
*
|
|
52
|
+
* E20_S10_T01 provenance-gating contract (the single authoritative filtering point for any
|
|
53
|
+
* architecture-view consumer — see the story's Acceptance Criteria): only `source: 'human'` or
|
|
54
|
+
* `source: 'ast'` nodes are ever rendered as full architecture nodes. `source: 'board'` nodes
|
|
55
|
+
* (mechanical Epic/Story restatements — see STUB_SCHEMA.md's "Where `board` Fits") are excluded
|
|
56
|
+
* from `nodes` here, but this is a READ-TIME filter only: `graph.json` itself is never modified by
|
|
57
|
+
* this reader, so `board` nodes remain available on disk for any other consumer that still wants
|
|
58
|
+
* board/PM traceability. Filtering lives here (rather than being duplicated in `architecture.js`,
|
|
59
|
+
* which just forwards this function's return value, or in `SADMap.jsx`) because this is already
|
|
60
|
+
* the single place that transforms the raw on-disk graph shape into the UI-facing shape — any
|
|
61
|
+
* future architecture-view consumer should call `readSADMap()` rather than reimplementing its own
|
|
62
|
+
* provenance filter.
|
|
63
|
+
*
|
|
64
|
+
* `status: superseded` nodes are also excluded, regardless of provenance (pre-existing behavior,
|
|
65
|
+
* unchanged by E20_S10_T01). Never throws — returns `{ nodes: [], edges: [], coverage: {shown: 0,
|
|
66
|
+
* hidden: 0, total: 0, needsRevalidation: 0} }` for a missing, empty, or malformed graph.json.
|
|
67
|
+
*
|
|
68
|
+
* E20_S10_T05 staleness/revalidation: a verified node MAY carry `needs_revalidation: true` (set by
|
|
69
|
+
* `scripts/populate-knowledge-graph.js`'s staleness check comparing a node's stored `content_hash`
|
|
70
|
+
* against its current source — see that script's `checkStaleness()`). This is surfaced two ways:
|
|
71
|
+
* per-node (`needsRevalidation` on the rendered node, for a UI badge) and aggregated into
|
|
72
|
+
* `coverage.needsRevalidation`, through the SAME pass as `shown`/`hidden`/`total`, per this
|
|
73
|
+
* function's existing never-drift contract. The flag is additive — it never removes a node from
|
|
74
|
+
* `nodes`, only marks it for a "needs re-verification" badge.
|
|
75
|
+
*
|
|
76
|
+
* E20_S10_T03 mixed-provenance edge handling: an unverified (`board`) node that is nonetheless an
|
|
77
|
+
* edge endpoint paired with a verified (`human`/`ast`) node is NOT dropped from `nodes` — it is
|
|
78
|
+
* still included, tagged `verified: false`, so `SADMap.jsx` can render it as a dimmed/"ghost" stub
|
|
79
|
+
* (label + provenance badge, no full detail) rather than silently dropping the edge or promoting
|
|
80
|
+
* the unverified node to full status. Every other `board` node (not an edge endpoint of a verified
|
|
81
|
+
* node) stays excluded per `E20_S10_T01`. Ghost-stub nodes are still counted as `hidden`, never
|
|
82
|
+
* `shown`, in `coverage` — they were never confirmed as architecture, only surfaced for edge
|
|
83
|
+
* continuity.
|
|
84
|
+
*
|
|
85
|
+
* @returns {{
|
|
86
|
+
* nodes: Array<{id: string, label: string, type: string, verified: boolean, source: string, needsRevalidation: boolean}>,
|
|
87
|
+
* edges: Array<{from: string, to: string, label: string}>,
|
|
88
|
+
* coverage: {shown: number, hidden: number, total: number, needsRevalidation: number}
|
|
89
|
+
* }}
|
|
45
90
|
*/
|
|
46
91
|
function readSADMap() {
|
|
47
92
|
try {
|
|
@@ -50,13 +95,50 @@ function readSADMap() {
|
|
|
50
95
|
const rawNodes = graph && Array.isArray(graph.nodes) ? graph.nodes : [];
|
|
51
96
|
const rawEdges = graph && Array.isArray(graph.edges) ? graph.edges : [];
|
|
52
97
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
98
|
+
const activeNodes = rawNodes.filter((node) => node && node.status !== 'superseded');
|
|
99
|
+
const activeNodeById = new Map(activeNodes.map((node) => [node.id, node]));
|
|
100
|
+
|
|
101
|
+
const verifiedNodes = activeNodes.filter(isVerifiedSource);
|
|
102
|
+
const verifiedIds = new Set(verifiedNodes.map((node) => node.id));
|
|
103
|
+
|
|
104
|
+
// E20_S10_T03: an unverified node earns a ghost-stub slot only when it is a genuine edge
|
|
105
|
+
// endpoint whose *other* endpoint is verified — never merely for being present in the graph.
|
|
106
|
+
// This keeps the bulk of unverified nodes hidden while preserving the traceability link a full
|
|
107
|
+
// drop would sever.
|
|
108
|
+
const ghostIds = new Set();
|
|
109
|
+
for (const edge of rawEdges) {
|
|
110
|
+
if (!edge) continue;
|
|
111
|
+
considerGhostEndpoint(edge.to, edge.from);
|
|
112
|
+
considerGhostEndpoint(edge.from, edge.to);
|
|
113
|
+
}
|
|
114
|
+
function considerGhostEndpoint(candidateId, otherId) {
|
|
115
|
+
if (!verifiedIds.has(otherId)) return; // other endpoint must be verified
|
|
116
|
+
if (verifiedIds.has(candidateId)) return; // candidate already fully rendered
|
|
117
|
+
const candidate = activeNodeById.get(candidateId);
|
|
118
|
+
if (candidate && !isVerifiedSource(candidate)) ghostIds.add(candidateId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const nodes = [
|
|
122
|
+
...verifiedNodes.map((node) => ({
|
|
56
123
|
id: node.id,
|
|
57
124
|
label: node.label,
|
|
58
125
|
type: node.type,
|
|
59
|
-
|
|
126
|
+
verified: true,
|
|
127
|
+
source: node.source,
|
|
128
|
+
needsRevalidation: node.needs_revalidation === true,
|
|
129
|
+
})),
|
|
130
|
+
...[...ghostIds].map((id) => {
|
|
131
|
+
const node = activeNodeById.get(id);
|
|
132
|
+
return {
|
|
133
|
+
id: node.id,
|
|
134
|
+
label: node.label,
|
|
135
|
+
type: node.type,
|
|
136
|
+
verified: false,
|
|
137
|
+
source: node.source,
|
|
138
|
+
needsRevalidation: false, // staleness only applies to verified (non-board) nodes
|
|
139
|
+
};
|
|
140
|
+
}),
|
|
141
|
+
];
|
|
60
142
|
|
|
61
143
|
const edges = rawEdges.map((edge) => ({
|
|
62
144
|
from: edge.from,
|
|
@@ -64,9 +146,18 @@ function readSADMap() {
|
|
|
64
146
|
label: edge.description || edge.type,
|
|
65
147
|
}));
|
|
66
148
|
|
|
67
|
-
|
|
149
|
+
// E20_S10_T02: coverage counts are derived from this same pass (activeNodes / verifiedNodes),
|
|
150
|
+
// never a separately cached computation, so they can never drift from what's actually
|
|
151
|
+
// rendered. `shown` counts only fully-verified nodes — ghost stubs never count as shown.
|
|
152
|
+
const shown = verifiedNodes.length;
|
|
153
|
+
const total = activeNodes.length;
|
|
154
|
+
const hidden = total - shown;
|
|
155
|
+
// E20_S10_T05: counted from the same verifiedNodes pass — never separately cached.
|
|
156
|
+
const needsRevalidation = verifiedNodes.filter((node) => node.needs_revalidation === true).length;
|
|
157
|
+
|
|
158
|
+
return { nodes, edges, coverage: { shown, hidden, total, needsRevalidation } };
|
|
68
159
|
} catch {
|
|
69
|
-
return { nodes: [], edges: [] };
|
|
160
|
+
return { nodes: [], edges: [], coverage: { shown: 0, hidden: 0, total: 0, needsRevalidation: 0 } };
|
|
70
161
|
}
|
|
71
162
|
}
|
|
72
163
|
|
|
@@ -3,14 +3,49 @@
|
|
|
3
3
|
* GET /health — server liveness check
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
6
8
|
const { Router } = require('express');
|
|
7
9
|
const { successResponse } = require('../response');
|
|
8
10
|
const { API_VERSION } = require('../response');
|
|
11
|
+
const { resolveProjectRoot } = require('../lib/resolve-project-root');
|
|
9
12
|
|
|
10
13
|
const router = Router();
|
|
11
14
|
|
|
12
15
|
const startTime = Date.now();
|
|
13
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the consuming project's name from its own root `package.json`, via the shared
|
|
19
|
+
* `resolveProjectRoot()` (see E47_S02) — never this monorepo's own `package.json` when running
|
|
20
|
+
* against a consumer install.
|
|
21
|
+
*
|
|
22
|
+
* Resolved once at module load, mirroring `startTime` above: the consuming project's own
|
|
23
|
+
* `package.json` does not change for the lifetime of a running server process.
|
|
24
|
+
*
|
|
25
|
+
* Graceful by design (E47_S06_T01): a missing project root, missing/unreadable `package.json`, a
|
|
26
|
+
* parse failure, or a missing/non-string `name` field all resolve to `null` rather than throwing or
|
|
27
|
+
* propagating "undefined" — the health check's liveness contract must never fail because project
|
|
28
|
+
* metadata could not be resolved, and the UI is expected to fall back to a sensible default title
|
|
29
|
+
* when this is `null`.
|
|
30
|
+
*
|
|
31
|
+
* @returns {string|null}
|
|
32
|
+
*/
|
|
33
|
+
function resolveProjectName() {
|
|
34
|
+
try {
|
|
35
|
+
const root = resolveProjectRoot();
|
|
36
|
+
const pkgRaw = fs.readFileSync(path.join(root, 'package.json'), 'utf8');
|
|
37
|
+
const pkg = JSON.parse(pkgRaw);
|
|
38
|
+
if (typeof pkg.name === 'string' && pkg.name.trim().length > 0) {
|
|
39
|
+
return pkg.name.trim();
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const projectName = resolveProjectName();
|
|
48
|
+
|
|
14
49
|
router.get('/', (req, res) => {
|
|
15
50
|
const uptime = (Date.now() - startTime) / 1000;
|
|
16
51
|
res.json(
|
|
@@ -18,6 +53,7 @@ router.get('/', (req, res) => {
|
|
|
18
53
|
status: 'ok',
|
|
19
54
|
version: API_VERSION,
|
|
20
55
|
uptime,
|
|
56
|
+
projectName,
|
|
21
57
|
})
|
|
22
58
|
);
|
|
23
59
|
});
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* E47_S04_T02 — capture step for `j.dashboard --snapshot`.
|
|
6
6
|
*
|
|
7
|
-
* Calls the dashboard API's `/v1/board`, `/v1/history`, and `/v1/
|
|
8
|
-
* each and writes their combined JSON to a single artifact, consumable by the
|
|
9
|
-
* step in `E47_S04_T03` (
|
|
10
|
-
*
|
|
7
|
+
* Calls the dashboard API's `/v1/board`, `/v1/history`, `/v1/architecture`, and `/v1/health` routes
|
|
8
|
+
* exactly once each and writes their combined JSON to a single artifact, consumable by the
|
|
9
|
+
* bundling/inlining step in `E47_S04_T03` (implemented) and, for `/v1/health` specifically, by
|
|
10
|
+
* `E47_S06_T02`'s snapshot `<title>` rewrite in `../../ui/scripts/build-snapshot-html.cjs`, which
|
|
11
|
+
* needs the captured project name to bake into the exported HTML's title at build time.
|
|
11
12
|
*
|
|
12
13
|
* ── Why an ad-hoc, in-process server instead of hitting an already-running one ──────────────────
|
|
13
14
|
* This script always spins up its own short-lived, ephemeral (`--port 0` by default) server
|
|
@@ -45,7 +46,8 @@
|
|
|
45
46
|
* "routes": {
|
|
46
47
|
* "board": <full API envelope from GET /v1/board, i.e. { data, meta, error }>,
|
|
47
48
|
* "history": <full API envelope from GET /v1/history, i.e. { data, meta, error }>,
|
|
48
|
-
* "architecture": <full API envelope from GET /v1/architecture, i.e. { data, meta, error }
|
|
49
|
+
* "architecture": <full API envelope from GET /v1/architecture, i.e. { data, meta, error }>,
|
|
50
|
+
* "health": <full API envelope from GET /v1/health, i.e. { data, meta, error }>
|
|
49
51
|
* }
|
|
50
52
|
* }
|
|
51
53
|
* Each `routes.<name>` value is the *full* envelope exactly as the route returned it (see
|
|
@@ -90,11 +92,12 @@ const path = require('path');
|
|
|
90
92
|
const http = require('http');
|
|
91
93
|
const fs = require('fs');
|
|
92
94
|
|
|
93
|
-
const ROUTES = ['/v1/board', '/v1/history', '/v1/architecture'];
|
|
95
|
+
const ROUTES = ['/v1/board', '/v1/history', '/v1/architecture', '/v1/health'];
|
|
94
96
|
const ROUTE_KEYS = {
|
|
95
97
|
'/v1/board': 'board',
|
|
96
98
|
'/v1/history': 'history',
|
|
97
99
|
'/v1/architecture': 'architecture',
|
|
100
|
+
'/v1/health': 'health',
|
|
98
101
|
};
|
|
99
102
|
|
|
100
103
|
function printUsage() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.loading-spinner{padding:2rem;text-align:center;color:#888;font-size:1rem}.error-message{padding:1rem;background:#fef2f2;border:1px solid #fca5a5;border-radius:4px;color:#b91c1c}.history-tab h2{margin-bottom:1rem}.history-empty{color:#888;padding:2rem;text-align:center}.history-list{list-style:none;padding:0;margin:0;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.history-entry{display:flex;align-items:center;gap:.75rem;padding:.65rem 1rem;cursor:pointer;border-bottom:1px solid #f3f4f6;transition:background .12s}.history-entry:last-child{border-bottom:none}.history-entry:hover,.history-entry:focus{background:#f0f7ff;outline:none}.history-entry.selected{background:#dbeafe}.history-badge{flex-shrink:0;font-size:.75rem;font-weight:600;padding:.2rem .55rem;border-radius:999px;white-space:nowrap}.badge-commit{background:#dbeafe;color:#1d4ed8}.badge-rapport{background:#dcfce7;color:#15803d}.history-subject{flex:1;font-size:.9rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#1f2937}.history-date{flex-shrink:0;font-size:.78rem;color:#9ca3af}.panel-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000040;z-index:100}.detail-panel{position:fixed;top:0;right:0;bottom:0;width:min(480px,100vw);background:#fff;box-shadow:-4px 0 24px #00000026;z-index:101;overflow-y:auto;padding:1.5rem;animation:slideIn .2s ease-out}@keyframes slideIn{0%{transform:translate(100%)}to{transform:translate(0)}}.panel-close{float:right;background:none;border:none;font-size:1.25rem;cursor:pointer;color:#6b7280;padding:.25rem;line-height:1}.panel-close:hover{color:#111827}.panel-title{margin:0 0 1rem;font-size:1.1rem;color:#1f2937}.panel-meta{display:grid;grid-template-columns:auto 1fr;gap:.3rem 1rem;font-size:.85rem;margin-bottom:1rem}.panel-meta dt{font-weight:600;color:#6b7280;white-space:nowrap}.panel-meta dd{margin:0;color:#1f2937;word-break:break-all}.panel-subject{font-size:.95rem;color:#1f2937}.panel-body{font-size:.82rem;white-space:pre-wrap;background:#f9fafb;border:1px solid #e5e7eb;border-radius:4px;padding:.75rem;max-height:40vh;overflow-y:auto}.panel-empty{color:#9ca3af;font-style:italic}.markdown-body{font-size:.9rem;line-height:1.6;color:#1f2937}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4{margin:1em 0 .4em;line-height:1.3}.markdown-body p{margin:.5em 0}.markdown-body code{background:#f3f4f6;padding:.1em .35em;border-radius:3px;font-size:.85em}.markdown-body pre{background:#f3f4f6;padding:.75rem;border-radius:4px;overflow-x:auto;font-size:.82em}.markdown-body pre code{background:none;padding:0}.markdown-body blockquote{border-left:3px solid #d1d5db;margin:.75em 0;padding:.25em 1em;color:#6b7280}.markdown-body ul,.markdown-body ol{padding-left:1.5em;margin:.5em 0}@media (max-width: 480px){.history-entry{flex-wrap:wrap;row-gap:var(--space-xs)}.history-subject{flex-basis:100%;order:3;white-space:normal}.detail-panel{box-sizing:border-box;padding:var(--space-md)}.panel-close{padding:var(--space-sm);font-size:1.5rem;display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:44px}.panel-meta{grid-template-columns:1fr;gap:.15rem 0}.panel-meta dt{margin-top:var(--space-xs)}.markdown-body code{overflow-wrap:break-word;word-break:break-word}.markdown-body table{display:block;max-width:100%;overflow-x:auto}}.board-view{display:flex;flex-direction:column;gap:1.5rem}.epic-card{background:#fff;border:1px solid #e5e7eb;border-radius:6px;padding:1rem}.epic-header{display:flex;align-items:center;gap:.75rem;margin-bottom:.75rem}.epic-title{font-size:1.1rem;font-weight:600;margin:0;cursor:pointer}.epic-id{color:#6b7280;font-size:.85rem}.stories-list{display:flex;flex-direction:column;gap:.75rem;padding-left:1rem}.story-card{background:#f9fafb;border:1px solid #e5e7eb;border-radius:4px;padding:.75rem}.story-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem}.story-title{font-size:1rem;font-weight:500;margin:0;cursor:pointer}.story-id{color:#6b7280;font-size:.8rem}.tasks-list{display:flex;flex-direction:column;gap:.4rem;padding-left:1rem}.task-card{background:#fff;border:1px solid #e5e7eb;border-radius:3px;padding:.5rem .75rem;display:flex;align-items:center;gap:.5rem}.task-title{font-size:.9rem;flex:1;cursor:pointer}.task-id{color:#9ca3af;font-size:.75rem}.task-assigned{color:#6b7280;font-size:.8rem}.status-badge{padding:.2rem .5rem;border-radius:9999px;font-size:.75rem;font-weight:500}.badge-pending{background:#f3f4f6;color:#374151}.badge-running{background:#dbeafe;color:#1d4ed8}.badge-in-progress{background:#ffedd5;color:#c2410c}.badge-passed{background:#dcfce7;color:#15803d}.badge-passed-remarks{background:#fff;color:#15803d;border:1px solid #dcfce7}.badge-done{background:#dbeafe;color:#1d4ed8}.badge-failed{background:#fee2e2;color:#b91c1c}.empty-state{padding:2rem;text-align:center;color:#6b7280}.status-filter{display:flex;flex-wrap:wrap;gap:.75rem;padding:.75rem 1rem;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px}.status-filter-option{display:flex;align-items:center;gap:.35rem;font-size:.85rem;color:#374151;cursor:pointer}@media (max-width: 480px){.epic-header,.story-header{flex-wrap:wrap;row-gap:var(--space-xs)}.epic-title,.story-title{flex-basis:100%}.task-card{flex-wrap:wrap;row-gap:var(--space-xs)}.task-card .task-title{flex-basis:100%}.stories-list,.tasks-list{padding-left:var(--space-sm)}}.kanban-board{display:flex;align-items:flex-start;gap:.75rem;overflow-x:auto;padding-bottom:.5rem}.kanban-column{flex:0 0 220px;width:220px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:.6rem;display:flex;flex-direction:column;gap:.5rem}.kanban-column-empty{flex:0 0 140px;width:140px;opacity:.6}.kanban-column-header{display:flex;align-items:center;justify-content:space-between;gap:.4rem}.kanban-column-title{font-size:.8rem;font-weight:600;color:#374151;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kanban-column-count{font-size:.7rem;color:#6b7280;background:#e5e7eb;border-radius:9999px;padding:.05rem .4rem}.kanban-column-empty-state{font-size:.75rem;color:#9ca3af;text-align:center;padding:.5rem 0}.kanban-column-items{display:flex;flex-direction:column;gap:.5rem}.kanban-card{background:#fff;border:1px solid #e5e7eb;border-radius:4px;padding:.5rem .6rem;display:flex;flex-direction:column;gap:.3rem}.kanban-card-header{display:flex;align-items:center;justify-content:space-between;gap:.4rem}.kanban-card-type{font-size:.65rem;text-transform:uppercase;letter-spacing:.03em;color:#9ca3af}.kanban-card-id{font-size:.7rem;color:#9ca3af}.kanban-card-title{font-size:.85rem;color:#111827;cursor:pointer}.kanban-card-queued{border-style:dashed;border-color:#c4b5fd;background:#fdfcff}.kanban-card-footer{display:flex;align-items:center;justify-content:space-between;gap:.4rem}.kanban-card-queued-marker{font-size:.65rem;text-transform:uppercase;letter-spacing:.03em;color:#6d28d9;background:#ede9fe;border:1px solid #ddd6fe;border-radius:9999px;padding:.05rem .4rem;white-space:nowrap;cursor:help}@media (max-width: 480px){.kanban-board{position:relative;gap:var(--space-sm);scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch}.kanban-column{flex:0 0 85%;width:85%;scroll-snap-align:start;scroll-snap-stop:always}.kanban-column-empty{flex:0 0 140px;width:140px}.kanban-board:after{content:"";position:sticky;top:0;right:0;flex:0 0 var(--space-xl);align-self:stretch;background:linear-gradient(to right,transparent,rgba(249,250,251,.95));pointer-events:none}}.architecture-tab{overflow-x:hidden}.architecture-tab h2{margin-bottom:1rem}.arch-empty{color:#9ca3af;font-style:italic}.arch-section{margin-bottom:2rem}.arch-section h3{font-size:1rem;font-weight:600;color:#374151;margin-bottom:.75rem;border-bottom:1px solid #e5e7eb;padding-bottom:.35rem}.tech-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.75rem}.tech-card{background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:.85rem 1rem;display:flex;flex-direction:column;gap:.3rem;box-shadow:0 1px 3px #0000000d}.tech-name{font-weight:600;font-size:.9rem;color:#1f2937}.tech-description{font-size:.78rem;color:#6b7280}.dep-table{width:100%;border-collapse:collapse;font-size:.875rem}.dep-table th{text-align:left;padding:.5rem .75rem;background:#f9fafb;border:1px solid #e5e7eb;font-weight:600;color:#374151}.dep-table td{padding:.45rem .75rem;border:1px solid #f3f4f6;color:#1f2937}.dep-table tr:hover td{background:#f9fafb}.dep-name{font-family:monospace}.dep-version{color:#6b7280;font-family:monospace}.dep-badge{display:inline-block;font-size:.7rem;font-weight:600;padding:.18rem .55rem;border-radius:999px;white-space:nowrap}.dep-runtime{background:#dbeafe;color:#1d4ed8}.dep-dev{background:#f3f4f6;color:#4b5563}.dep-table-wrap{overflow-x:auto}.sad-map-scroll{width:100%;min-height:320px;overflow:auto;-webkit-overflow-scrolling:touch;border:1px solid var(--border, #e2e8f0);border-radius:8px;background:var(--bg-secondary, #f8fafc)}.sad-map-svg{display:block;min-width:800px;min-height:500px;padding:8px}.sad-empty{color:var(--text-muted, #94a3b8);font-style:italic}@media (max-width: 480px){.architecture-tab{padding-bottom:var(--space-sm)}.arch-section{margin-bottom:var(--space-lg)}.arch-section h3{padding-bottom:var(--space-xs)}.tech-grid{gap:var(--space-sm)}.tech-card{padding:var(--space-sm) var(--space-md)}.sad-map-scroll{min-height:180px}}.entry-list-empty{color:#888;padding:2rem;text-align:center}.entry-list{list-style:none;padding:0;margin:0;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.entry-list-item{display:flex;align-items:center;gap:.75rem;padding:.65rem 1rem;cursor:pointer;border-bottom:1px solid #f3f4f6;transition:background .12s}.entry-list-item:last-child{border-bottom:none}.entry-list-item:hover,.entry-list-item:focus{background:#f0f7ff;outline:none}.entry-list-item.selected{background:#dbeafe}.entry-badge{flex-shrink:0;font-size:.75rem;font-weight:600;padding:.2rem .55rem;border-radius:999px;white-space:nowrap;text-transform:capitalize;background:#ede9fe;color:#6d28d9}.entry-title{flex:1;font-size:.9rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#1f2937}.entry-date{flex-shrink:0;font-size:.78rem;color:#9ca3af}@media (max-width: 480px){.entry-list-item{flex-wrap:wrap;row-gap:var(--space-xs)}.entry-title{flex-basis:100%;order:3;white-space:normal}}.pagination-controls{display:flex;align-items:center;justify-content:center;gap:var(--space-md);margin-top:var(--space-md)}.pagination-btn{padding:.4rem .9rem;font-size:.85rem;border:1px solid #e5e7eb;border-radius:6px;background:#fff;color:#1f2937;cursor:pointer}.pagination-btn:hover:not(:disabled){background:#f0f7ff;border-color:#93c5fd}.pagination-btn:disabled{opacity:.45;cursor:not-allowed}.pagination-status{font-size:.82rem;color:#6b7280;white-space:nowrap}@media (max-width: 480px){.pagination-controls{gap:var(--space-sm)}.pagination-btn{padding:.5rem .85rem;font-size:.9rem}.pagination-status{font-size:.78rem}}.rapports-search-input{display:block;width:100%;box-sizing:border-box;padding:.55rem .75rem;margin-bottom:var(--space-md);font-size:.9rem;border:1px solid #e5e7eb;border-radius:6px;color:#1f2937}.rapports-search-input:focus{outline:none;border-color:#93c5fd;box-shadow:0 0 0 2px #3b82f640}@media (max-width: 480px){.rapports-search-input{font-size:1rem;padding:.65rem .75rem}}:root{--space-xs: .25rem;--space-sm: .5rem;--space-md: 1rem;--space-lg: 1.5rem;--space-xl: 2rem;--breakpoint-mobile: 480px;--breakpoint-tablet: 768px}body{font-family:sans-serif;margin:0;background:#f5f5f5}.app{max-width:1200px;margin:0 auto;padding:1rem}.tabs{display:flex;gap:0;border-bottom:1px solid #ddd}.tab-btn{padding:.6rem 1.2rem;border:none;background:transparent;cursor:pointer;font-size:1rem;color:#555;border-bottom:2px solid transparent}.tab-btn.active{color:#2563eb;border-bottom-color:#2563eb;font-weight:600}.tab-content{padding:1rem 0}@media (max-width: 480px){.app{width:100%;box-sizing:border-box;padding:var(--space-sm)}.tabs{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch}.tab-btn{flex:0 0 auto;padding:var(--space-sm) var(--space-md)}}
|
|
1
|
+
.loading-spinner{padding:2rem;text-align:center;color:#888;font-size:1rem}.error-message{padding:1rem;background:#fef2f2;border:1px solid #fca5a5;border-radius:4px;color:#b91c1c}.history-tab h2{margin-bottom:1rem}.history-empty{color:#888;padding:2rem;text-align:center}.history-list{list-style:none;padding:0;margin:0;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.history-entry{display:flex;align-items:center;gap:.75rem;padding:.65rem 1rem;cursor:pointer;border-bottom:1px solid #f3f4f6;transition:background .12s}.history-entry:last-child{border-bottom:none}.history-entry:hover,.history-entry:focus{background:#f0f7ff;outline:none}.history-entry.selected{background:#dbeafe}.history-badge{flex-shrink:0;font-size:.75rem;font-weight:600;padding:.2rem .55rem;border-radius:999px;white-space:nowrap}.badge-commit{background:#dbeafe;color:#1d4ed8}.badge-rapport{background:#dcfce7;color:#15803d}.history-subject{flex:1;font-size:.9rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#1f2937}.history-date{flex-shrink:0;font-size:.78rem;color:#9ca3af}.panel-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000040;z-index:100}.detail-panel{position:fixed;top:0;right:0;bottom:0;width:min(480px,100vw);background:#fff;box-shadow:-4px 0 24px #00000026;z-index:101;overflow-y:auto;padding:1.5rem;animation:slideIn .2s ease-out}@keyframes slideIn{0%{transform:translate(100%)}to{transform:translate(0)}}.panel-close{float:right;background:none;border:none;font-size:1.25rem;cursor:pointer;color:#6b7280;padding:.25rem;line-height:1}.panel-close:hover{color:#111827}.panel-title{margin:0 0 1rem;font-size:1.1rem;color:#1f2937}.panel-meta{display:grid;grid-template-columns:auto 1fr;gap:.3rem 1rem;font-size:.85rem;margin-bottom:1rem}.panel-meta dt{font-weight:600;color:#6b7280;white-space:nowrap}.panel-meta dd{margin:0;color:#1f2937;word-break:break-all}.panel-subject{font-size:.95rem;color:#1f2937}.panel-body{font-size:.82rem;white-space:pre-wrap;background:#f9fafb;border:1px solid #e5e7eb;border-radius:4px;padding:.75rem;max-height:40vh;overflow-y:auto}.panel-empty{color:#9ca3af;font-style:italic}.markdown-body{font-size:.9rem;line-height:1.6;color:#1f2937}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4{margin:1em 0 .4em;line-height:1.3}.markdown-body p{margin:.5em 0}.markdown-body code{background:#f3f4f6;padding:.1em .35em;border-radius:3px;font-size:.85em}.markdown-body pre{background:#f3f4f6;padding:.75rem;border-radius:4px;overflow-x:auto;font-size:.82em}.markdown-body pre code{background:none;padding:0}.markdown-body blockquote{border-left:3px solid #d1d5db;margin:.75em 0;padding:.25em 1em;color:#6b7280}.markdown-body ul,.markdown-body ol{padding-left:1.5em;margin:.5em 0}@media (max-width: 480px){.history-entry{flex-wrap:wrap;row-gap:var(--space-xs)}.history-subject{flex-basis:100%;order:3;white-space:normal}.detail-panel{box-sizing:border-box;padding:var(--space-md)}.panel-close{padding:var(--space-sm);font-size:1.5rem;display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:44px}.panel-meta{grid-template-columns:1fr;gap:.15rem 0}.panel-meta dt{margin-top:var(--space-xs)}.markdown-body code{overflow-wrap:break-word;word-break:break-word}.markdown-body table{display:block;max-width:100%;overflow-x:auto}}.board-view{display:flex;flex-direction:column;gap:1.5rem}.epic-card{background:#fff;border:1px solid #e5e7eb;border-radius:6px;padding:1rem}.epic-header{display:flex;align-items:center;gap:.75rem;margin-bottom:.75rem}.epic-title{font-size:1.1rem;font-weight:600;margin:0;cursor:pointer}.epic-id{color:#6b7280;font-size:.85rem}.stories-list{display:flex;flex-direction:column;gap:.75rem;padding-left:1rem}.story-card{background:#f9fafb;border:1px solid #e5e7eb;border-radius:4px;padding:.75rem}.story-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem}.story-title{font-size:1rem;font-weight:500;margin:0;cursor:pointer}.story-id{color:#6b7280;font-size:.8rem}.tasks-list{display:flex;flex-direction:column;gap:.4rem;padding-left:1rem}.task-card{background:#fff;border:1px solid #e5e7eb;border-radius:3px;padding:.5rem .75rem;display:flex;align-items:center;gap:.5rem}.task-title{font-size:.9rem;flex:1;cursor:pointer}.task-id{color:#9ca3af;font-size:.75rem}.task-assigned{color:#6b7280;font-size:.8rem}.status-badge{padding:.2rem .5rem;border-radius:9999px;font-size:.75rem;font-weight:500}.badge-pending{background:#f3f4f6;color:#374151}.badge-running{background:#dbeafe;color:#1d4ed8}.badge-in-progress{background:#ffedd5;color:#c2410c}.badge-passed{background:#dcfce7;color:#15803d}.badge-passed-remarks{background:#fff;color:#15803d;border:1px solid #dcfce7}.badge-done{background:#dbeafe;color:#1d4ed8}.badge-failed{background:#fee2e2;color:#b91c1c}.empty-state{padding:2rem;text-align:center;color:#6b7280}.status-filter{display:flex;flex-wrap:wrap;gap:.75rem;padding:.75rem 1rem;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px}.status-filter-option{display:flex;align-items:center;gap:.35rem;font-size:.85rem;color:#374151;cursor:pointer}@media (max-width: 480px){.epic-header,.story-header{flex-wrap:wrap;row-gap:var(--space-xs)}.epic-title,.story-title{flex-basis:100%}.task-card{flex-wrap:wrap;row-gap:var(--space-xs)}.task-card .task-title{flex-basis:100%}.stories-list,.tasks-list{padding-left:var(--space-sm)}}.kanban-board{display:flex;align-items:flex-start;gap:.75rem;overflow-x:auto;padding-bottom:.5rem}.kanban-column{flex:0 0 220px;width:220px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:.6rem;display:flex;flex-direction:column;gap:.5rem}.kanban-column-empty{flex:0 0 140px;width:140px;opacity:.6}.kanban-column-header{display:flex;align-items:center;justify-content:space-between;gap:.4rem}.kanban-column-title{font-size:.8rem;font-weight:600;color:#374151;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kanban-column-count{font-size:.7rem;color:#6b7280;background:#e5e7eb;border-radius:9999px;padding:.05rem .4rem}.kanban-column-empty-state{font-size:.75rem;color:#9ca3af;text-align:center;padding:.5rem 0}.kanban-column-items{display:flex;flex-direction:column;gap:.5rem}.kanban-card{background:#fff;border:1px solid #e5e7eb;border-radius:4px;padding:.5rem .6rem;display:flex;flex-direction:column;gap:.3rem}.kanban-card-header{display:flex;align-items:center;justify-content:space-between;gap:.4rem}.kanban-card-type{font-size:.65rem;text-transform:uppercase;letter-spacing:.03em;color:#9ca3af}.kanban-card-id{font-size:.7rem;color:#9ca3af}.kanban-card-title{font-size:.85rem;color:#111827;cursor:pointer}.kanban-card-queued{border-style:dashed;border-color:#c4b5fd;background:#fdfcff}.kanban-card-footer{display:flex;align-items:center;justify-content:space-between;gap:.4rem}.kanban-card-queued-marker{font-size:.65rem;text-transform:uppercase;letter-spacing:.03em;color:#6d28d9;background:#ede9fe;border:1px solid #ddd6fe;border-radius:9999px;padding:.05rem .4rem;white-space:nowrap;cursor:help}@media (max-width: 480px){.kanban-board{position:relative;gap:var(--space-sm);scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch}.kanban-column{flex:0 0 85%;width:85%;scroll-snap-align:start;scroll-snap-stop:always}.kanban-column-empty{flex:0 0 140px;width:140px}.kanban-board:after{content:"";position:sticky;top:0;right:0;flex:0 0 var(--space-xl);align-self:stretch;background:linear-gradient(to right,transparent,rgba(249,250,251,.95));pointer-events:none}}.architecture-tab{overflow-x:hidden}.architecture-tab h2{margin-bottom:1rem}.arch-empty{color:#9ca3af;font-style:italic}.arch-section{margin-bottom:2rem}.arch-section h3{font-size:1rem;font-weight:600;color:#374151;margin-bottom:.75rem;border-bottom:1px solid #e5e7eb;padding-bottom:.35rem}.tech-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.75rem}.tech-card{background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:.85rem 1rem;display:flex;flex-direction:column;gap:.3rem;box-shadow:0 1px 3px #0000000d}.tech-name{font-weight:600;font-size:.9rem;color:#1f2937}.tech-description{font-size:.78rem;color:#6b7280}.dep-table{width:100%;border-collapse:collapse;font-size:.875rem}.dep-table th{text-align:left;padding:.5rem .75rem;background:#f9fafb;border:1px solid #e5e7eb;font-weight:600;color:#374151}.dep-table td{padding:.45rem .75rem;border:1px solid #f3f4f6;color:#1f2937}.dep-table tr:hover td{background:#f9fafb}.dep-name{font-family:monospace}.dep-version{color:#6b7280;font-family:monospace}.dep-badge{display:inline-block;font-size:.7rem;font-weight:600;padding:.18rem .55rem;border-radius:999px;white-space:nowrap}.dep-runtime{background:#dbeafe;color:#1d4ed8}.dep-dev{background:#f3f4f6;color:#4b5563}.dep-table-wrap{overflow-x:auto}.sad-map-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;margin-bottom:.6rem}.sad-type-chip{--chip-color: #94a3b8;display:inline-flex;align-items:center;gap:.35rem;font-size:.75rem;font-weight:600;color:#374151;background:#fff;border:1px solid #e5e7eb;border-radius:999px;padding:.25rem .65rem;cursor:pointer;line-height:1.2}.sad-type-chip.is-collapsed{background:#f3f4f6;border-color:var(--chip-color)}.sad-type-chip-dot{width:8px;height:8px;border-radius:50%;background:var(--chip-color);flex-shrink:0}.sad-reset-view-btn{font-size:.75rem;font-weight:600;color:#374151;background:#fff;border:1px solid #e5e7eb;border-radius:6px;padding:.25rem .65rem;cursor:pointer;margin-left:auto}.sad-map-scroll{width:100%;min-height:320px;overflow:hidden;-webkit-overflow-scrolling:touch;border:1px solid var(--border, #e2e8f0);border-radius:8px;background:var(--bg-secondary, #f8fafc)}.sad-map-svg{display:block;width:100%;cursor:grab}.sad-map-svg:active{cursor:grabbing}.sad-empty{color:var(--text-muted, #94a3b8);font-style:italic}.sad-coverage-indicator{font-size:.78rem;color:#6b7280;margin:0 0 .5rem}.sad-coverage-shown{font-weight:600;color:#374151}.sad-coverage-hidden{color:#9ca3af}.sad-coverage-stale{color:#b45309;font-weight:600}.sad-empty-state{display:flex;flex-direction:column;gap:.35rem}.sad-empty-detail{font-size:.8rem;color:#9ca3af;margin:0}.sad-ghost-legend{display:flex;align-items:center;gap:.4rem;font-size:.75rem;color:#9ca3af;margin:0 0 .6rem}.sad-ghost-legend-swatch{display:inline-block;width:12px;height:12px;flex-shrink:0;border-radius:3px;background:#94a3b8;opacity:.4;border:1.5px dashed #1f2937}@media (max-width: 480px){.architecture-tab{padding-bottom:var(--space-sm)}.arch-section{margin-bottom:var(--space-lg)}.arch-section h3{padding-bottom:var(--space-xs)}.tech-grid{gap:var(--space-sm)}.tech-card{padding:var(--space-sm) var(--space-md)}.sad-map-scroll{min-height:180px}}.entry-list-empty{color:#888;padding:2rem;text-align:center}.entry-list{list-style:none;padding:0;margin:0;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.entry-list-item{display:flex;align-items:center;gap:.75rem;padding:.65rem 1rem;cursor:pointer;border-bottom:1px solid #f3f4f6;transition:background .12s}.entry-list-item:last-child{border-bottom:none}.entry-list-item:hover,.entry-list-item:focus{background:#f0f7ff;outline:none}.entry-list-item.selected{background:#dbeafe}.entry-badge{flex-shrink:0;font-size:.75rem;font-weight:600;padding:.2rem .55rem;border-radius:999px;white-space:nowrap;text-transform:capitalize;background:#ede9fe;color:#6d28d9}.entry-title{flex:1;font-size:.9rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#1f2937}.entry-date{flex-shrink:0;font-size:.78rem;color:#9ca3af}@media (max-width: 480px){.entry-list-item{flex-wrap:wrap;row-gap:var(--space-xs)}.entry-title{flex-basis:100%;order:3;white-space:normal}}.pagination-controls{display:flex;align-items:center;justify-content:center;gap:var(--space-md);margin-top:var(--space-md)}.pagination-btn{padding:.4rem .9rem;font-size:.85rem;border:1px solid #e5e7eb;border-radius:6px;background:#fff;color:#1f2937;cursor:pointer}.pagination-btn:hover:not(:disabled){background:#f0f7ff;border-color:#93c5fd}.pagination-btn:disabled{opacity:.45;cursor:not-allowed}.pagination-status{font-size:.82rem;color:#6b7280;white-space:nowrap}@media (max-width: 480px){.pagination-controls{gap:var(--space-sm)}.pagination-btn{padding:.5rem .85rem;font-size:.9rem}.pagination-status{font-size:.78rem}}.rapports-search-input{display:block;width:100%;box-sizing:border-box;padding:.55rem .75rem;margin-bottom:var(--space-md);font-size:.9rem;border:1px solid #e5e7eb;border-radius:6px;color:#1f2937}.rapports-search-input:focus{outline:none;border-color:#93c5fd;box-shadow:0 0 0 2px #3b82f640}@media (max-width: 480px){.rapports-search-input{font-size:1rem;padding:.65rem .75rem}}:root{--space-xs: .25rem;--space-sm: .5rem;--space-md: 1rem;--space-lg: 1.5rem;--space-xl: 2rem;--breakpoint-mobile: 480px;--breakpoint-tablet: 768px}body{font-family:sans-serif;margin:0;background:#f5f5f5}.app{max-width:1200px;margin:0 auto;padding:1rem}.tabs{display:flex;gap:0;border-bottom:1px solid #ddd}.tab-btn{padding:.6rem 1.2rem;border:none;background:transparent;cursor:pointer;font-size:1rem;color:#555;border-bottom:2px solid transparent}.tab-btn.active{color:#2563eb;border-bottom-color:#2563eb;font-weight:600}.tab-content{padding:1rem 0}@media (max-width: 480px){.app{width:100%;box-sizing:border-box;padding:var(--space-sm)}.tabs{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch}.tab-btn{flex:0 0 auto;padding:var(--space-sm) var(--space-md)}}
|