@ikenga/pkg-tasks 0.2.0 → 0.4.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.
@@ -0,0 +1,137 @@
1
+ // Assignee roster — single source of truth for who a task can be owned by, used
2
+ // by both the create form (owner field) and the detail pane's Reassign picker.
3
+ //
4
+ // Two assignee kinds map onto the `tasks` columns: `assigned_to` (the id —
5
+ // an email for a human, an agent id like `cfo-agent` for an agent) and
6
+ // `assignee_type` ('human' | 'agent'). `assigneeIsAgent` in shared.js also
7
+ // treats a trailing `-agent` as the agent convention, so keep agent ids
8
+ // suffixed `-agent`.
9
+ //
10
+ // ## Roster resolution
11
+ //
12
+ // The roster is INJECTABLE via `hostContext.royaltiSuite.tasksRoster` at
13
+ // iframe-mount time. The shell delivers hostContext through the AppBridge
14
+ // `connectBridge` return value and the `onContextChange` callback (see
15
+ // bridge.js / app.js). The expected shape is:
16
+ //
17
+ // hostContext.royaltiSuite.tasksRoster = {
18
+ // humans: [{ value: string, label: string }], // email → display name
19
+ // agents: [{ id: string, label: string }], // agent-id → display name
20
+ // }
21
+ //
22
+ // When `tasksRoster` is present and well-formed (both arrays non-empty) the
23
+ // configured list takes full precedence over the static defaults below.
24
+ // When absent or malformed the static CURRENT_USER + AGENT_ROSTER fallback
25
+ // remains active — so the pkg works unchanged today and on every future
26
+ // install that hasn't run `skill-tasks setup` yet.
27
+ //
28
+ // ## Shell hook needed (WP-10 contract side)
29
+ //
30
+ // The shell must read `.atelier/skill-tasks/roster.json` from the current
31
+ // project dir (the directory passed as --project / CLAUDE_PROJECT_DIR) and
32
+ // inject it as `hostContext.royaltiSuite.tasksRoster` when building the
33
+ // hostContext object for this pkg's iframe (pkg-iframe-host.tsx). The JSON
34
+ // file shape mirrors the `tasksRoster` field above:
35
+ // { "humans": [{"value":"...", "label":"..."}],
36
+ // "agents": [{"id":"...", "label":"..."}] }
37
+ // Written by `skill-tasks` `setup` (WP-06 setup lifecycle); the shell need
38
+ // only pass it through — it must not transform or cache it.
39
+
40
+ // The logged-in human. TODO(hello@royalti.io): thread from
41
+ // hostContext.royaltiAuth once it carries the user email (mirrors the same TODO
42
+ // in tasks-view.js — this module is now the one place that literal lives).
43
+ export const CURRENT_USER = 'hello@royalti.io';
44
+
45
+ /** @typedef {{ id: string, label: string }} AgentEntry */
46
+
47
+ /** @type {AgentEntry[]} */
48
+ export const AGENT_ROSTER = [
49
+ { id: 'cfo-agent', label: 'CFO · Finance' },
50
+ { id: 'cmo-agent', label: 'CMO · Marketing' },
51
+ { id: 'coo-agent', label: 'COO · Operations' },
52
+ { id: 'content-agent', label: 'Content' },
53
+ { id: 'outbound-agent', label: 'Outbound' },
54
+ ];
55
+
56
+ /** @typedef {{ value: string, label: string, type: 'human' | 'agent' }} AssigneeOption */
57
+
58
+ /**
59
+ * @typedef {{ value: string, label: string }} HumanEntry
60
+ * @typedef {{ humans: HumanEntry[], agents: AgentEntry[] }} TasksRoster
61
+ */
62
+
63
+ /**
64
+ * Validate and return a configured roster from `hostContext.royaltiSuite.tasksRoster`,
65
+ * or `null` if absent / malformed. A valid roster has both `humans` and `agents`
66
+ * as non-empty arrays.
67
+ *
68
+ * @param {unknown} [hostContext]
69
+ * @returns {TasksRoster | null}
70
+ */
71
+ export function resolveRoster(hostContext) {
72
+ const raw = /** @type {any} */ (hostContext)?.royaltiSuite?.tasksRoster;
73
+ if (!raw) return null;
74
+ const { humans, agents } = raw;
75
+ if (
76
+ !Array.isArray(humans) || humans.length === 0 ||
77
+ !Array.isArray(agents) || agents.length === 0
78
+ ) {
79
+ return null;
80
+ }
81
+ // Basic per-entry shape validation — skip malformed entries rather than reject.
82
+ const validHumans = humans.filter(
83
+ (h) => h && typeof h.value === 'string' && typeof h.label === 'string',
84
+ );
85
+ const validAgents = agents.filter(
86
+ (a) => a && typeof a.id === 'string' && typeof a.label === 'string',
87
+ );
88
+ if (validHumans.length === 0 || validAgents.length === 0) return null;
89
+ return { humans: validHumans, agents: validAgents };
90
+ }
91
+
92
+ /**
93
+ * Flat option list for an assignee <select>. "Me" first (human), then each
94
+ * configured agent. The empty-value "Unassigned" sentinel is added by the
95
+ * caller's <select> so this list stays purely the real assignees.
96
+ *
97
+ * Accepts an optional `hostContext` object. When it carries a valid
98
+ * `royaltiSuite.tasksRoster`, the configured roster wins. Without it (or when
99
+ * the roster is absent/malformed) the static CURRENT_USER + AGENT_ROSTER
100
+ * defaults are used, so existing call sites calling the no-arg form remain
101
+ * correct without modification.
102
+ *
103
+ * @param {unknown} [hostContext]
104
+ * @returns {AssigneeOption[]}
105
+ */
106
+ export function assigneeOptions(hostContext) {
107
+ const roster = resolveRoster(hostContext);
108
+ if (roster) {
109
+ return [
110
+ ...roster.humans.map((h) => ({ value: h.value, label: h.label, type: /** @type {'human'} */ ('human') })),
111
+ ...roster.agents.map((a) => ({ value: a.id, label: a.label, type: /** @type {'agent'} */ ('agent') })),
112
+ ];
113
+ }
114
+ // Static fallback — unchanged behaviour.
115
+ return [
116
+ { value: CURRENT_USER, label: 'Me', type: 'human' },
117
+ ...AGENT_ROSTER.map((a) => ({ value: a.id, label: a.label, type: /** @type {'agent'} */ ('agent') })),
118
+ ];
119
+ }
120
+
121
+ /**
122
+ * Resolve a picked `assigned_to` value back to its `assignee_type`. Falls back
123
+ * to the `-agent` naming convention for ids not in the roster (e.g. legacy rows
124
+ * or a future configured agent not yet reflected here).
125
+ *
126
+ * Accepts an optional `hostContext`; threads it through to `assigneeOptions` so
127
+ * the configured roster (when present) is consulted first.
128
+ *
129
+ * @param {string} value
130
+ * @param {unknown} [hostContext]
131
+ * @returns {'human' | 'agent'}
132
+ */
133
+ export function assigneeTypeFor(value, hostContext) {
134
+ const match = assigneeOptions(hostContext).find((o) => o.value === value);
135
+ if (match) return match.type;
136
+ return value.endsWith('-agent') ? 'agent' : 'human';
137
+ }
@@ -33,7 +33,8 @@ export async function connectBridge({ name, version, onContextChange }) {
33
33
 
34
34
  app.onerror = (err) => console.error('[tasks] bridge error', err);
35
35
  // Theme is NOT applied here — app.js mirrors it from the parent <html>.
36
- // We still forward context so live Supabase/auth updates reach the app.
36
+ // We still forward context so live activeFeature (side-menu) updates reach
37
+ // the app; data flows through host.dbQuery/dbExec, not the context payload.
37
38
  app.onhostcontextchanged = (ctx) => {
38
39
  onContextChange?.(ctx);
39
40
  };
@@ -129,6 +130,28 @@ export async function hostDbQuery(sql, params = []) {
129
130
  return Array.isArray(sc.rows) ? sc.rows : [];
130
131
  }
131
132
 
133
+ /**
134
+ * Write to the local `pa.db` via the host's `host.dbExec` verb (write-path WP).
135
+ * INSERT/UPDATE/DELETE only — the shell rejects reads/DDL, gates on the pkg
136
+ * declaring `capabilities.sqlite`, and scopes the target table to the pkg's
137
+ * declared `permissions['sqlite.tables']`. Resolves on success; throws on a
138
+ * closed/failed bridge so callers can surface the error in the mutation layer.
139
+ *
140
+ * sql: string — a single INSERT/UPDATE/DELETE statement with `?` params
141
+ * params: SqlValue[] — positional bind values
142
+ */
143
+ export async function hostDbExec(sql, params = []) {
144
+ if (!app) throw new Error('[tasks] bridge not connected — db_exec unavailable');
145
+ const res = await app.callServerTool({
146
+ name: 'host.dbExec',
147
+ arguments: { sql, params },
148
+ });
149
+ const sc = res?.structuredContent;
150
+ if (!sc || sc.ok !== true) {
151
+ throw new Error(sc?.error ?? res?.content?.[0]?.text ?? 'host.dbExec failed');
152
+ }
153
+ }
154
+
132
155
  /** Read the current hostContext snapshot. */
133
156
  export function getContext() {
134
157
  return app?.getHostContext() ?? null;
@@ -26,10 +26,6 @@ declare module 'https://esm.sh/@tanstack/react-query@5?deps=react@19.0.0' {
26
26
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
27
  export const useQueryClient: any;
28
28
  }
29
- declare module 'https://esm.sh/@supabase/supabase-js@2' {
30
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
- export const createClient: any;
32
- }
33
29
  declare module 'https://esm.sh/@modelcontextprotocol/ext-apps@1.7.1/app-with-deps' {
34
30
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
31
  export const App: any;
@@ -3,12 +3,13 @@
3
3
  // migration 0025_tasks_domain.sql): every selected column exists.
4
4
  //
5
5
  // WP-04 read-swap: reads go through the host's `host.dbQuery` verb (local
6
- // pa.db) instead of an in-iframe supabase-js client. The status-update WRITE
7
- // (task-detail-pane.js) still uses supabase-js moving it needs a host write
8
- // verb that does not exist yet (follow-up WP).
6
+ // pa.db) instead of an in-iframe supabase-js client. Write-path WP: the
7
+ // status-update WRITE goes through `host.dbExec` (see `updateTaskStatus`), so
8
+ // this pkg no longer depends on supabase-js at all.
9
9
 
10
- import { hostDbQuery } from './bridge.js';
10
+ import { hostDbExec, hostDbQuery } from './bridge.js';
11
11
  import { queryKeys } from './query-keys.js';
12
+ import { CURRENT_USER } from './assignees.js';
12
13
 
13
14
  // pa.db stores former Postgres array/json columns as TEXT (the Pg→SQLite
14
15
  // down-map, shell migration 0025). `tags` arrives as a string, not a JS array
@@ -191,3 +192,87 @@ export function blockingTaskQuery(blockingId) {
191
192
  enabled: !!blockingId,
192
193
  };
193
194
  }
195
+
196
+ /**
197
+ * Write a task's status to the local pa.db via `host.dbExec` (write-path WP).
198
+ * `completed_at` is set to now when moving to `completed` and cleared to NULL
199
+ * otherwise, so a non-completed task never carries a stale completion stamp.
200
+ * The host scopes this write to the pkg's declared `sqlite.tables` (`tasks`).
201
+ *
202
+ * @param {string} taskId
203
+ * @param {TaskStatus} status
204
+ * @returns {Promise<void>}
205
+ */
206
+ export async function updateTaskStatus(taskId, status) {
207
+ const completedAt = status === 'completed' ? new Date().toISOString() : null;
208
+ await hostDbExec('UPDATE tasks SET status = ?, completed_at = ? WHERE id = ?', [
209
+ status,
210
+ completedAt,
211
+ taskId,
212
+ ]);
213
+ }
214
+
215
+ /**
216
+ * @typedef {Object} CreateTaskInput
217
+ * @property {string} title required (NOT NULL in 0025)
218
+ * @property {string|null} [assignedTo] email (human) or agent id; null = unassigned
219
+ * @property {'human'|'agent'|null} [assigneeType]
220
+ * @property {TaskPriority} [priority] defaults 'medium'
221
+ * @property {string|null} [dueDate] ISO timestamp or null
222
+ * @property {string|null} [description]
223
+ * @property {string|null} [category]
224
+ */
225
+
226
+ /**
227
+ * Insert a new task into the local pa.db via `host.dbExec` (write-path WP).
228
+ * Now that `host.dbExec` allows a real INSERT (the table is in the pkg's
229
+ * declared `sqlite.tables`), creation no longer has to round-trip through the
230
+ * agent. The id is a client-generated uuid; created_at/updated_at are stamped
231
+ * now; status defaults to 'pending'. Only `id` + `title` are NOT NULL in
232
+ * migration 0025 — every other column is nullable or DB-defaulted, so we write
233
+ * just what the form collected and let SQLite default the rest.
234
+ *
235
+ * @param {CreateTaskInput} input
236
+ * @returns {Promise<string>} the new task id
237
+ */
238
+ export async function createTask(input) {
239
+ const id = crypto.randomUUID();
240
+ const now = new Date().toISOString();
241
+ await hostDbExec(
242
+ 'INSERT INTO tasks (id, title, description, status, priority, assigned_to, assignee_type, category, due_date, created_at, updated_at, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
243
+ [
244
+ id,
245
+ input.title,
246
+ input.description ?? null,
247
+ 'pending',
248
+ input.priority ?? 'medium',
249
+ input.assignedTo ?? null,
250
+ input.assigneeType ?? null,
251
+ input.category ?? null,
252
+ input.dueDate ?? null,
253
+ now,
254
+ now,
255
+ CURRENT_USER,
256
+ ],
257
+ );
258
+ return id;
259
+ }
260
+
261
+ /**
262
+ * Reassign a task to a different human/agent via `host.dbExec`. `assigned_to`
263
+ * was display-only across every view until now; this is the first write of it.
264
+ * `updated_at` is bumped so the staleness/triage logic (which keys off it)
265
+ * doesn't treat a just-reassigned task as stale. Pass `assignedTo = null` to
266
+ * clear the owner (unassign).
267
+ *
268
+ * @param {string} taskId
269
+ * @param {string|null} assignedTo
270
+ * @param {'human'|'agent'|null} assigneeType
271
+ * @returns {Promise<void>}
272
+ */
273
+ export async function reassignTask(taskId, assignedTo, assigneeType) {
274
+ await hostDbExec(
275
+ 'UPDATE tasks SET assigned_to = ?, assignee_type = ?, updated_at = ? WHERE id = ?',
276
+ [assignedTo, assigneeType, new Date().toISOString(), taskId],
277
+ );
278
+ }
@@ -67,7 +67,7 @@ export function groupTasks(tasks, showAutoclosed) {
67
67
  }
68
68
 
69
69
  // ─── In-body view switcher (Round 16 · D-2 / D-3) ───────────────────────────
70
- /** @typedef {'tasks'|'agenda'|'triage'} TaskView */
70
+ /** @typedef {'tasks'|'agenda'|'triage'|'sweeper'|'done'} TaskView */
71
71
 
72
72
  // ─── Agenda / Today (D-2) ────────────────────────────────────────────────────
73
73
  /** @typedef {'me'|'agent'|'silent'|'deadline'} AgendaLane */
@@ -1,5 +1,5 @@
1
1
  // Auto-derived from tasks.css — CSS-as-string so it loads via the script path
2
- // (WebKitGTK can't load <link>/fetch subresources from the about:srcdoc the
2
+ // (WebKitGTK cannot load link/fetch subresources from the about:srcdoc the
3
3
  // shell mounts — only inlined scripts work; see index.html). app.js injects
4
- // this as an inline <style> (allowed by style-src 'unsafe-inline').
5
- export default "/* Tasks screen — Ikenga Rung 3 / Batch 3 (08-tasks.html)\n * Reuses tokens from src/lib/ikenga/tokens.css. No new tokens.\n */\n\n/* === Frame ================================================== */\n.tk-frame {\n border: 1px solid var(--border);\n border-radius: var(--radius-lg);\n background: var(--bg-surface);\n overflow: hidden;\n box-shadow: var(--shadow-2);\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.tk-frame-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--space-4);\n padding: var(--space-4) var(--space-5);\n border-bottom: 1px solid var(--border-soft);\n background: linear-gradient(180deg, var(--tint-bg-active, var(--bg-surface)) 0%, var(--bg-surface) 100%);\n}\n.tk-frame-title-wrap { display: flex; align-items: center; gap: var(--space-2); }\n.tk-frame-title-mark { width: 18px; height: 18px; color: var(--tint-fg-active, var(--primary)); }\n.tk-frame-title {\n font-family: var(--font-display);\n font-weight: 500;\n font-size: var(--text-h3);\n margin: 0;\n color: var(--fg);\n}\n.tk-frame-sub {\n margin-top: 2px;\n font-size: var(--text-caption);\n color: var(--fg-muted);\n}\n.tk-frame-count {\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--fg-muted);\n font-weight: 400;\n margin-left: 6px;\n}\n\n/* === Filter bar ============================================= */\n.tk-filterbar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-2);\n padding: var(--space-3) var(--space-5);\n border-bottom: 1px solid var(--border-soft);\n background: var(--bg-sunken);\n}\n.tk-filterbar .input-search-wrap { position: relative; display: inline-flex; }\n.tk-filterbar .input-search-wrap svg {\n position: absolute;\n left: 8px;\n top: 50%;\n transform: translateY(-50%);\n width: 13px;\n height: 13px;\n color: var(--fg-faint);\n}\n.tk-filterbar input[type='text'] {\n width: 280px;\n height: 28px;\n padding: 0 8px 0 28px;\n background: var(--bg-base);\n border: 1px solid var(--border-soft);\n border-radius: var(--radius-sm);\n color: var(--fg);\n font-family: inherit;\n font-size: 11.5px;\n}\n.tk-filterbar select {\n height: 28px;\n font-size: 11.5px;\n padding: 0 var(--space-2);\n background: var(--bg-base);\n border: 1px solid var(--border-soft);\n border-radius: var(--radius-sm);\n color: var(--fg);\n font-family: inherit;\n}\n.tk-filterbar .label {\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--fg-faint);\n letter-spacing: 0.06em;\n text-transform: uppercase;\n}\n.tk-filterbar .spacer { flex: 1; }\n.tk-toggle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 28px;\n padding: 0 10px;\n background: var(--bg-base);\n border: 1px solid var(--border-soft);\n border-radius: var(--radius-sm);\n color: var(--fg-muted);\n font-family: inherit;\n font-size: 11px;\n cursor: pointer;\n}\n.tk-toggle:hover { color: var(--fg); border-color: var(--fg-faint); }\n.tk-toggle.is-on {\n color: var(--live);\n background: var(--live-soft);\n border-color: color-mix(in srgb, var(--live) 30%, var(--border));\n}\n.tk-toggle .checkbox {\n width: 11px;\n height: 11px;\n border: 1px solid currentColor;\n border-radius: 2px;\n display: inline-grid;\n place-items: center;\n flex-shrink: 0;\n}\n.tk-toggle.is-on .checkbox::after {\n content: '✓';\n font-size: 9px;\n line-height: 1;\n color: currentColor;\n}\n\n/* === Master/detail split ==================================== */\n.tk-split {\n --list-w: 360px;\n display: grid;\n grid-template-columns: minmax(280px, var(--list-w)) 4px minmax(420px, 1fr);\n /* Bind the single row to the split's own height. Without this the implicit\n * row is `auto` (content-sized), so a tall list/detail grows the row past the\n * split and gets clipped by overflow:hidden — neither pane scrolls. */\n grid-template-rows: minmax(0, 1fr);\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n.tk-list {\n overflow-y: auto;\n /* Grid items default to min-height:auto (won't shrink below content), which\n * defeats overflow:auto. Allow shrink so the list scrolls within its track. */\n min-height: 0;\n background: var(--bg-surface);\n}\n.tk-divider {\n background: var(--border-soft);\n cursor: col-resize;\n}\n.tk-divider:hover { background: var(--tint-fg-active, var(--primary)); }\n.tk-detail {\n background: var(--bg-base);\n overflow-y: auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* === Group dividers ========================================= */\n.tk-group-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px var(--space-4) 6px var(--space-3);\n background: var(--bg-sunken);\n border-bottom: 1px solid var(--border-soft);\n border-top: 1px solid var(--border-soft);\n font-family: var(--font-mono);\n font-size: 9.5px;\n color: var(--fg-faint);\n letter-spacing: 0.1em;\n text-transform: uppercase;\n position: sticky;\n top: 0;\n z-index: 4;\n cursor: pointer;\n user-select: none;\n}\n.tk-group-head:first-child { border-top: 0; }\n.tk-group-head .ct {\n color: var(--fg-muted);\n font-variant-numeric: tabular-nums;\n}\n.tk-group-head.is-overdue { color: var(--danger); }\n.tk-group-head.is-overdue .ct { color: var(--danger); }\n.tk-group-head.is-autoclosed { color: var(--live); }\n.tk-group-head.is-autoclosed .ct { color: var(--live); }\n.tk-group-head.is-collapsed { background: var(--bg-base); }\n.tk-group-head .chev {\n width: 10px;\n height: 10px;\n color: var(--fg-faint);\n transition: transform var(--motion-fast) var(--ease-calm);\n}\n.tk-group-head.is-collapsed .chev { transform: rotate(-90deg); }\n.tk-group-label {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n/* === Task row =============================================== */\n.tk-row {\n display: grid;\n grid-template-columns: 14px 1fr auto;\n gap: var(--space-3);\n padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--border-soft);\n cursor: pointer;\n position: relative;\n background: transparent;\n text-align: left;\n width: 100%;\n border-left: 0;\n border-right: 0;\n border-top: 0;\n font: inherit;\n color: inherit;\n transition: background var(--motion-fast) var(--ease-calm);\n}\n.tk-row:hover { background: var(--bg-raised); }\n.tk-row.is-on { background: var(--bg-raised); }\n.tk-row.is-on::before {\n content: '';\n position: absolute;\n left: 0;\n top: 8px;\n bottom: 8px;\n width: 2px;\n border-radius: 2px;\n background: var(--tint-fg-active, var(--primary));\n}\n.tk-row .pri-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n margin-top: 5px;\n background: var(--fg-faint);\n flex-shrink: 0;\n}\n.tk-row .pri-dot.is-critical {\n background: var(--danger);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--danger) 20%, transparent);\n}\n.tk-row .pri-dot.is-high { background: var(--achievement); }\n.tk-row .pri-dot.is-medium { background: var(--systemic); }\n.tk-row .pri-dot.is-low { background: var(--fg-faint); }\n.tk-row .body {\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.tk-row .title {\n font-size: var(--text-body-sm);\n color: var(--fg);\n font-weight: 500;\n line-height: 1.35;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n.tk-row.is-completed .title {\n color: var(--fg-muted);\n text-decoration: line-through;\n text-decoration-thickness: 1px;\n}\n.tk-row .meta {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n font-family: var(--font-mono);\n font-size: 9.5px;\n color: var(--fg-faint);\n letter-spacing: 0.04em;\n}\n.tk-row .meta .cat {\n background: var(--bg-base);\n border: 1px solid var(--border-soft);\n color: var(--fg-muted);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n text-transform: lowercase;\n}\n.tk-row .right {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 4px;\n flex-shrink: 0;\n}\n.tk-row .due {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg-muted);\n letter-spacing: 0.04em;\n white-space: nowrap;\n}\n.tk-row .due.is-overdue { color: var(--danger); font-weight: 500; }\n.tk-row .due.is-today { color: var(--achievement); }\n\n/* === Status badge =========================================== */\n.tk-badge {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-family: var(--font-mono);\n font-size: 9.5px;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: var(--radius-xs);\n border: 1px solid var(--border-soft);\n color: var(--fg-muted);\n background: var(--bg-base);\n white-space: nowrap;\n}\n.tk-badge.is-pending { color: var(--fg-muted); }\n.tk-badge.is-in_progress {\n color: var(--live);\n background: var(--live-soft);\n border-color: color-mix(in srgb, var(--live) 30%, var(--border-soft));\n}\n.tk-badge.is-blocked {\n color: var(--danger);\n background: color-mix(in srgb, var(--danger) 12%, transparent);\n border-color: color-mix(in srgb, var(--danger) 30%, var(--border-soft));\n}\n.tk-badge.is-completed {\n color: var(--live);\n background: var(--live-soft);\n border-color: color-mix(in srgb, var(--live) 30%, var(--border-soft));\n}\n.tk-badge.is-cancelled { color: var(--fg-faint); }\n.tk-badge .dot {\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background: currentColor;\n}\n\n/* === Auto-close badge ======================================= */\n.tk-autoclose {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.06em;\n color: var(--live);\n background: var(--live-soft);\n border: 1px solid color-mix(in srgb, var(--live) 25%, var(--border-soft));\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n white-space: nowrap;\n}\n.tk-autoclose svg { width: 9px; height: 9px; }\n\n/* === Assignee chips ========================================= */\n.tk-assignee {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-family: var(--font-mono);\n font-size: 9.5px;\n letter-spacing: 0.04em;\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n border: 1px solid var(--border-soft);\n background: var(--bg-base);\n color: var(--fg-muted);\n white-space: nowrap;\n}\n.tk-assignee.is-agent {\n color: var(--agent);\n background: var(--agent-soft);\n border-color: color-mix(in srgb, var(--agent) 30%, var(--border-soft));\n}\n.tk-assignee .avatar {\n width: 11px;\n height: 11px;\n border-radius: 50%;\n background: var(--bg-raised);\n color: var(--fg);\n display: inline-grid;\n place-items: center;\n font-size: 7.5px;\n font-weight: 600;\n text-transform: uppercase;\n}\n.tk-assignee.is-agent .avatar {\n background: color-mix(in srgb, var(--agent) 24%, transparent);\n color: var(--agent);\n}\n.tk-assignee .dot {\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background: var(--agent);\n}\n\n/* === Exec mode pill ========================================= */\n.tk-execmode {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: var(--fg-faint);\n border: 1px dashed var(--border-soft);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n background: transparent;\n}\n.tk-execmode.is-autonomous {\n color: var(--agent);\n border-color: color-mix(in srgb, var(--agent) 30%, var(--border-soft));\n}\n.tk-execmode.is-approval_required {\n color: var(--achievement);\n border-color: color-mix(in srgb, var(--achievement) 35%, var(--border-soft));\n}\n.tk-execmode.is-report {\n color: var(--systemic);\n border-color: color-mix(in srgb, var(--systemic) 30%, var(--border-soft));\n}\n\n/* === Detail panel =========================================== */\n.tk-det-head {\n padding: var(--space-4) var(--space-5);\n border-bottom: 1px solid var(--border-soft);\n background: linear-gradient(180deg, var(--tint-bg-active, var(--bg-surface)) 0%, var(--bg-base) 100%);\n}\n.tk-det-topline {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-3);\n margin-bottom: 8px;\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--fg-faint);\n letter-spacing: 0.04em;\n}\n.tk-det-topline .id {\n background: var(--bg-base);\n border: 1px solid var(--border-soft);\n padding: 2px 6px;\n border-radius: var(--radius-xs);\n color: var(--fg);\n}\n.tk-det-actions { display: flex; gap: 4px; align-items: center; }\n.tk-det-title {\n font-family: var(--font-display);\n font-weight: 500;\n font-size: var(--text-h3);\n margin: 0;\n color: var(--fg);\n line-height: 1.25;\n}\n.tk-det-meta-row {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-2);\n margin-top: var(--space-3);\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--fg-faint);\n letter-spacing: 0.04em;\n}\n.tk-det-meta-row .sep { color: var(--fg-faint); }\n.tk-det-meta-row .pri-label { color: var(--achievement); display: inline-flex; align-items: center; gap: 4px; }\n.tk-det-meta-row .pri-label.is-critical { color: var(--danger); }\n.tk-det-meta-row .pri-label.is-high { color: var(--achievement); }\n.tk-det-meta-row .pri-label.is-medium { color: var(--systemic); }\n.tk-det-meta-row .pri-label.is-low { color: var(--fg-muted); }\n.tk-det-meta-row .pri-label .dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: currentColor;\n}\n.tk-det-meta-row .due-text { color: var(--fg-muted); }\n.tk-det-meta-row .due-text.is-overdue { color: var(--danger); }\n.tk-det-body {\n padding: var(--space-5);\n display: flex;\n flex-direction: column;\n gap: var(--space-5);\n flex: 1;\n}\n.tk-det-grid {\n display: grid;\n grid-template-columns: 110px 1fr;\n gap: 10px var(--space-4);\n font-size: var(--text-body-sm);\n}\n.tk-det-grid dt {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg-faint);\n letter-spacing: 0.1em;\n text-transform: uppercase;\n align-self: center;\n}\n.tk-det-grid dd {\n margin: 0;\n color: var(--fg);\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-2);\n font-size: var(--text-body-sm);\n}\n.tk-det-grid dd code {\n font-family: var(--font-mono);\n font-size: 11.5px;\n background: var(--bg-sunken);\n border: 1px solid var(--border-soft);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n color: var(--fg);\n}\n.tk-section-label {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--fg-faint);\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n margin-bottom: 6px;\n}\n.tk-section-label .ct {\n font-family: var(--font-mono);\n font-size: 9.5px;\n color: var(--fg-muted);\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.04em;\n text-transform: none;\n}\n.tk-deferred-pill {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: var(--fg-faint);\n border: 1px dashed var(--border);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n background: transparent;\n}\n\n.tk-desc {\n font-size: var(--text-body-sm);\n color: var(--fg-muted);\n line-height: 1.6;\n background: var(--bg-surface);\n border: 1px solid var(--border-soft);\n border-radius: var(--radius-md);\n padding: var(--space-3) var(--space-4);\n white-space: pre-wrap;\n}\n\n/* === Progress bar =========================================== */\n.tk-progress {\n flex: 1;\n height: 6px;\n background: var(--bg-sunken);\n border-radius: 3px;\n overflow: hidden;\n border: 1px solid var(--border-soft);\n min-width: 120px;\n}\n.tk-progress > span {\n display: block;\n height: 100%;\n background: var(--live);\n border-radius: 3px;\n transition: width var(--motion-fast) var(--ease-calm);\n}\n\n/* === Evidence card ========================================== */\n.tk-evidence {\n border: 1px solid color-mix(in srgb, var(--live) 30%, var(--border-soft));\n background: var(--live-soft);\n border-radius: var(--radius-md);\n padding: var(--space-3) var(--space-4);\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n.tk-evidence-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-3);\n}\n.tk-evidence .rule-chip {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-family: var(--font-mono);\n font-size: 9.5px;\n letter-spacing: 0.04em;\n color: var(--live);\n background: var(--bg-surface);\n border: 1px solid color-mix(in srgb, var(--live) 30%, var(--border-soft));\n padding: 2px 6px;\n border-radius: var(--radius-xs);\n}\n.tk-evidence .rule-chip svg { width: 10px; height: 10px; }\n.tk-evidence .rule-chip.is-flag {\n color: var(--achievement);\n border-color: color-mix(in srgb, var(--achievement) 30%, var(--border-soft));\n background: var(--bg-surface);\n}\n.tk-evidence .timestamp {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg-muted);\n letter-spacing: 0.04em;\n}\n.tk-evidence .body {\n font-size: var(--text-body-sm);\n color: var(--fg);\n line-height: 1.55;\n}\n\n/* === Source-ref chips ======================================= */\n.tk-source-row {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n.tk-src {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--fg-muted);\n background: var(--bg-sunken);\n border: 1px solid var(--border-soft);\n padding: 3px 7px;\n border-radius: var(--radius-sm);\n text-decoration: none;\n letter-spacing: 0.02em;\n cursor: pointer;\n}\n.tk-src:hover { color: var(--fg); border-color: var(--fg-faint); }\n.tk-src svg { width: 11px; height: 11px; flex-shrink: 0; color: var(--fg-faint); }\n.tk-src.is-email svg { color: var(--mail-fg, var(--achievement)); }\n.tk-src.is-session svg { color: var(--agent); }\n.tk-src.is-git svg { color: var(--systemic); }\n\n/* === Subtasks =============================================== */\n.tk-subtasks { display: flex; flex-direction: column; gap: 4px; }\n.tk-sub-row {\n display: grid;\n grid-template-columns: auto 1fr auto;\n align-items: center;\n gap: var(--space-2);\n padding: 6px var(--space-3);\n background: var(--bg-surface);\n border: 1px solid var(--border-soft);\n border-radius: var(--radius-sm);\n font-size: var(--text-body-sm);\n cursor: pointer;\n text-align: left;\n width: 100%;\n font: inherit;\n color: inherit;\n transition: background var(--motion-fast) var(--ease-calm);\n}\n.tk-sub-row:hover { background: var(--bg-raised); }\n.tk-sub-row .name {\n color: var(--fg);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.tk-sub-row.is-completed .name {\n color: var(--fg-muted);\n text-decoration: line-through;\n}\n.tk-sub-row .due {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg-faint);\n}\n\n/* === Activity timeline ====================================== */\n.tk-timeline {\n display: flex;\n flex-direction: column;\n gap: 0;\n border-left: 1px dashed var(--border);\n margin-left: 6px;\n padding-left: var(--space-4);\n position: relative;\n}\n.tk-tl-item {\n position: relative;\n padding: 6px 0;\n font-size: var(--text-body-sm);\n color: var(--fg-muted);\n}\n.tk-tl-item::before {\n content: '';\n position: absolute;\n left: calc(-1 * var(--space-4) - 4px);\n top: 12px;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--bg-sunken);\n border: 1px solid var(--border);\n}\n.tk-tl-item.is-mark::before {\n background: var(--tint-fg-active, var(--primary));\n border-color: var(--tint-fg-active, var(--primary));\n}\n.tk-tl-item.is-ok::before {\n background: var(--live);\n border-color: var(--live);\n}\n.tk-tl-item .when {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg-faint);\n letter-spacing: 0.04em;\n margin-right: 6px;\n}\n.tk-tl-item .actor {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg);\n background: var(--bg-sunken);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n border: 1px solid var(--border-soft);\n margin-right: 6px;\n}\n.tk-tl-item .actor.is-agent { color: var(--agent); }\n\n/* === Action footer ========================================== */\n.tk-action-bar {\n border-top: 1px solid var(--border-soft);\n background: var(--bg-sunken);\n padding: var(--space-3) var(--space-5);\n display: flex;\n align-items: center;\n gap: var(--space-2);\n margin-top: auto;\n}\n.tk-action-bar .spacer { flex: 1; }\n\n/* === Empty state in detail =================================== */\n.tk-empty {\n flex: 1;\n display: grid;\n place-items: center;\n color: var(--fg-faint);\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n}\n\n/* === Density variants for $taskId standalone (Section B) ===== */\n.tk-detail-pane.is-compact .tk-det-head { padding: var(--space-3) var(--space-4); }\n.tk-detail-pane.is-compact .tk-det-title { font-size: var(--text-h4); }\n.tk-detail-pane.is-compact .tk-det-body { padding: var(--space-4); gap: var(--space-4); }\n.tk-detail-pane.is-compact .tk-det-grid { grid-template-columns: 90px 1fr; }\n.tk-detail-pane.is-side .tk-det-head { padding: var(--space-3); }\n.tk-detail-pane.is-side .tk-det-title { font-size: 13.5px; line-height: 1.35; }\n.tk-detail-pane.is-side .tk-det-body { padding: var(--space-3); gap: var(--space-3); }\n.tk-detail-pane.is-side .tk-det-grid { display: none; }\n.tk-detail-pane.is-side .tk-section-label { margin-bottom: 4px; }\n\n/* === In-body view switcher (D-2 / D-3) ====================== */\n.ip-tabs {\n display: flex;\n align-items: center;\n gap: 0;\n padding: 0 var(--space-3);\n height: var(--tab-h, 38px);\n border-bottom: 1px solid var(--border-soft);\n background: var(--bg-sunken);\n}\n.ip-tab {\n height: var(--tab-h, 38px);\n padding: 0 var(--space-3);\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: var(--font-body);\n font-size: var(--text-body-sm);\n color: var(--fg-faint);\n border: 0;\n background: transparent;\n cursor: pointer;\n border-bottom: 2px solid transparent;\n margin-bottom: -1px;\n}\n.ip-tab svg { width: 13px; height: 13px; opacity: 0.8; }\n.ip-tab:hover { color: var(--fg-muted); }\n.ip-tab.is-on { color: var(--fg); border-bottom-color: var(--tint-fg-active, var(--primary)); }\n.ip-tab.is-on svg { opacity: 1; color: var(--tint-fg-active, var(--primary)); }\n.ip-tab:focus-visible,\n.tk-toggle:focus-visible,\n.tk-group-head:focus-visible,\n.tk-row:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }\n.ip-tab-badge {\n font-family: var(--font-mono);\n font-size: 9.5px;\n line-height: 1;\n color: var(--achievement);\n background: var(--achievement-soft);\n border: 1px solid color-mix(in srgb, var(--achievement) 30%, var(--border-soft));\n border-radius: var(--radius-pill, 999px);\n padding: 2px 5px;\n}\n\n/* === Agenda / Today (D-2) =================================== */\n/* AgendaView root — rendered directly inside .tk-frame (which is overflow:hidden),\n * so it must be the scroll region itself: grow to fill the leftover height, allow\n * shrink (min-height:0), scroll its own overflow. Mirrors the Browse page's\n * `1fr` + overflow-y:auto leaf (shell .ccfg-list/.ccfg-detail). Without this the\n * agenda is clipped by the frame and can't scroll. */\n.ag-wrap { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-5); max-width: 920px; }\n.ag-head {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--space-3);\n margin-bottom: var(--space-5);\n}\n.ag-date { font-family: var(--font-display); font-weight: 500; font-size: var(--text-h3); color: var(--fg); }\n.ag-summary { font-family: var(--font-mono); font-size: 10.5px; color: var(--fg-faint); letter-spacing: 0.04em; }\n.ag-summary b { color: var(--fg-muted); font-weight: 500; }\n.ag-filter-note { color: var(--achievement); font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.04em; }\n.ag-grid { display: grid; grid-template-columns: 54px 1fr; column-gap: var(--space-3); }\n.ag-time { font-family: var(--font-mono); font-size: 10px; color: var(--fg-faint); text-align: right; padding-top: 8px; letter-spacing: 0.04em; }\n.ag-lane { min-width: 0; padding-bottom: var(--space-3); }\n.ag-block {\n padding: 8px var(--space-3);\n border-radius: var(--radius-sm);\n border: 1px solid var(--border-soft);\n background: var(--bg-surface);\n border-left: 3px solid var(--fg-faint);\n margin-bottom: 6px;\n}\n.ag-block:last-child { margin-bottom: 0; }\n.ag-block.is-me { border-left-color: var(--primary); }\n.ag-block.is-agent { border-left-color: var(--agent); background: var(--agent-soft); }\n.ag-block.is-silent { border-left-color: var(--systemic); border-style: dashed; background: transparent; }\n.ag-block.is-deadline { border-left-color: var(--danger); background: color-mix(in srgb, var(--danger) 8%, transparent); }\n.ag-block.is-done { opacity: 0.6; }\n.ag-block .t { font-size: var(--text-body-sm); color: var(--fg); font-weight: 500; line-height: 1.4; }\n.ag-block.is-done .t { text-decoration: line-through; text-decoration-thickness: 1px; color: var(--fg-muted); }\n.ag-block .m { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 5px; }\n.ag-now { grid-column: 1 / -1; position: relative; height: 0; border-top: 2px solid var(--live); margin: 4px 0 10px; }\n.ag-now::after {\n content: 'now';\n position: absolute;\n right: 0;\n top: -8px;\n font-family: var(--font-mono);\n font-size: 8.5px;\n letter-spacing: 0.06em;\n color: var(--live);\n background: var(--bg-base);\n padding: 0 5px;\n}\n.ag-now::before { content: ''; position: absolute; left: -5px; top: -3px; width: 6px; height: 6px; border-radius: 50%; background: var(--live); }\n\n/* === Triage / Health (D-3) ================================== */\n/* TriageView root — same as .ag-wrap: own scroll region inside the overflow-hidden\n * frame (grow to fill, min-height:0, scroll). */\n.tr-wrap { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-5); max-width: 980px; }\n.tr-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-3); margin-bottom: var(--space-5); }\n.tr-stat { padding: var(--space-3) var(--space-4); border: 1px solid var(--border-soft); border-radius: var(--radius-md); background: var(--bg-surface); }\n/* Numbers stay --fg (12:1+ in every theme/mode) — the colored border carries\n the semantic. --achievement big-number measured 2.57:1 on a light surface. */\n.tr-stat .n { font-family: var(--font-display); font-weight: 500; font-size: 28px; line-height: 1; color: var(--fg); font-variant-numeric: tabular-nums; }\n.tr-stat .k { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--fg-faint); margin-top: 8px; }\n.tr-stat .sub { font-family: var(--font-mono); font-size: 9px; color: var(--fg-faint); letter-spacing: 0.02em; margin-top: 3px; }\n.tr-stat.is-danger { border-color: color-mix(in srgb, var(--danger) 35%, var(--border-soft)); }\n.tr-stat.is-warn { border-color: color-mix(in srgb, var(--achievement) 35%, var(--border-soft)); }\n.tr-stat.is-sys { border-color: color-mix(in srgb, var(--systemic) 35%, var(--border-soft)); }\n.tr-bucket { margin-bottom: var(--space-5); }\n.tr-bucket-head {\n display: flex;\n align-items: center;\n gap: var(--space-2);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n color: var(--fg-faint);\n margin-bottom: var(--space-2);\n}\n.tr-bucket-head .ct { color: var(--fg-muted); font-variant-numeric: tabular-nums; }\n.tr-bucket-head::after { content: ''; flex: 1; height: 1px; background: var(--border-soft); }\n.tr-mini {\n display: grid;\n grid-template-columns: auto 1fr auto auto;\n align-items: center;\n gap: var(--space-3);\n padding: 8px var(--space-3);\n border-bottom: 1px solid var(--border-soft);\n}\n.tr-mini:last-child { border-bottom: 0; }\n.tr-mini .title { font-size: var(--text-body-sm); color: var(--fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }\n.tr-mini .age { font-family: var(--font-mono); font-size: 10px; color: var(--fg-muted); letter-spacing: 0.04em; white-space: nowrap; }\n.tr-mini .age.is-bad { color: var(--danger); }\n.tr-more { font-family: var(--font-mono); font-size: 10px; color: var(--fg-faint); letter-spacing: 0.04em; padding: 6px var(--space-3); }\n.tr-health {\n display: flex;\n align-items: center;\n gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border: 1px dashed var(--border);\n border-radius: var(--radius-md);\n background: var(--bg-sunken);\n font-size: var(--text-body-sm);\n color: var(--fg-muted);\n line-height: 1.55;\n}\n.tr-health .score { font-family: var(--font-display); font-weight: 500; font-size: var(--text-h2); color: var(--achievement); line-height: 1; flex-shrink: 0; }\n\n/* === De-Tailwinded utilities ================================== */\n/* The source app leaned on a handful of Tailwind utility classes for layout\n * chrome around the locked .tk-* / .ag-* / .tr-* visuals. With no Tailwind in\n * the no-build pkg, those are reproduced here as a tiny, scoped utility set\n * (named after their Tailwind origin so the htm transcription reads 1:1).\n * Everything load-bearing is still a .tk-*/.ag-*/.tr-* rule above. */\n\n/* Root layout shell (was: `flex h-full flex-col p-5`) */\n.tk-screen {\n display: flex;\n height: 100%;\n flex-direction: column;\n padding: var(--space-5);\n}\n\n/* Loading / error / empty inline states (was Tailwind utility soup) */\n.tk-loading {\n display: flex;\n align-items: center;\n gap: var(--space-2);\n padding: var(--space-4);\n font-size: var(--text-body-sm);\n color: var(--fg-muted);\n}\n.tk-error {\n margin: var(--space-4);\n display: flex;\n align-items: flex-start;\n gap: var(--space-2);\n border-radius: var(--radius-md);\n border: 1px solid color-mix(in srgb, var(--danger) 50%, transparent);\n background: color-mix(in srgb, var(--danger) 10%, transparent);\n padding: var(--space-3);\n font-size: var(--text-body-sm);\n color: var(--danger);\n}\n.tk-error svg { flex-shrink: 0; margin-top: 2px; }\n.tk-error .t { font-weight: 500; }\n.tk-error .d { font-size: var(--text-caption); opacity: 0.8; margin-top: 2px; }\n.tk-empty-box {\n margin: var(--space-4);\n display: flex;\n height: 128px;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-md);\n border: 1px dashed var(--border);\n font-size: var(--text-body-sm);\n color: var(--fg-muted);\n}\n\n/* Spinner (was: `animate-spin`) */\n.tk-spin { animation: tk-spin 1s linear infinite; }\n@keyframes tk-spin { to { transform: rotate(360deg); } }\n\n/* Detail-pane status select inline-styled in source; kept inline in the htm. */\n\n/* === Button (was: src/components/Button.tsx, Tailwind utilities) ============ */\n.tk-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n font-family: inherit;\n font-weight: 500;\n white-space: nowrap;\n cursor: pointer;\n transition: background var(--motion-fast, 120ms) var(--ease-calm, ease),\n color var(--motion-fast, 120ms) var(--ease-calm, ease),\n opacity var(--motion-fast, 120ms) var(--ease-calm, ease);\n}\n.tk-btn svg { flex-shrink: 0; }\n.tk-btn:disabled { opacity: 0.5; pointer-events: none; }\n/* sizes */\n.tk-btn.sz-md { height: 32px; padding: 0 12px; font-size: var(--text-body-sm); border-radius: var(--radius-md); }\n.tk-btn.sz-sm { height: 28px; padding: 0 10px; font-size: var(--text-caption); border-radius: var(--radius-md); gap: 4px; }\n/* variants */\n.tk-btn.v-default {\n background: var(--primary);\n color: var(--primary-fg);\n border: 1px solid transparent;\n}\n.tk-btn.v-default:hover:not(:disabled) { opacity: 0.9; }\n.tk-btn.v-outline {\n background: transparent;\n color: var(--fg);\n border: 1px solid var(--border);\n}\n.tk-btn.v-outline:hover:not(:disabled) { background: var(--bg-raised); }\n.tk-btn.v-ghost {\n background: transparent;\n color: var(--fg);\n border: 1px solid transparent;\n}\n.tk-btn.v-ghost:hover:not(:disabled) { background: var(--bg-raised); }\n.tk-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }\n\n/* Mutation error line in the detail pane footer. */\n.tk-mut-error {\n padding: 0 var(--space-5) var(--space-3);\n font-size: 11px;\n color: var(--danger);\n}\n";
4
+ // this as an inline style element (allowed by style-src unsafe-inline).
5
+ export default "/* Tasks screen — Ikenga Rung 3 / Batch 3 (08-tasks.html)\n * Reuses tokens from src/lib/ikenga/tokens.css. No new tokens.\n */\n\n/* === Frame ================================================== */\n.tk-frame {\n /* No card chrome inside the shell — the pkg pane IS the container, so the\n * content fills it flush (mirrors the design's `#tk-host > .frame` reset).\n * No border / radius / shadow / surrounding padding. */\n background: var(--bg-surface);\n overflow: hidden;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.tk-frame-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-4);\n /* Slim single-row action bar (R-header): the sidebar carries domain + view\n * nav, so this bar holds only the active-view label + the New task action. */\n padding: var(--space-2) var(--space-5);\n border-bottom: 1px solid var(--border-soft);\n background: linear-gradient(180deg, var(--tint-bg-active, var(--bg-surface)) 0%, var(--bg-surface) 100%);\n}\n.tk-frame-title-wrap { display: flex; align-items: center; gap: var(--space-2); }\n.tk-frame-title-mark { width: 15px; height: 15px; color: var(--tint-fg-active, var(--primary)); }\n.tk-frame-title {\n font-family: var(--font-display);\n font-weight: 500;\n font-size: var(--text-body);\n margin: 0;\n color: var(--fg);\n}\n.tk-frame-sub {\n margin-top: 2px;\n font-size: var(--text-caption);\n color: var(--fg-muted);\n}\n.tk-frame-count {\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--fg-muted);\n font-weight: 400;\n margin-left: 6px;\n}\n\n/* === Filter bar ============================================= */\n.tk-filterbar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-2);\n padding: var(--space-3) var(--space-5);\n border-bottom: 1px solid var(--border-soft);\n background: var(--bg-sunken);\n}\n.tk-filterbar .input-search-wrap { position: relative; display: inline-flex; }\n.tk-filterbar .input-search-wrap svg {\n position: absolute;\n left: 8px;\n top: 50%;\n transform: translateY(-50%);\n width: 13px;\n height: 13px;\n color: var(--fg-faint);\n}\n.tk-filterbar input[type='text'] {\n width: 280px;\n height: 28px;\n padding: 0 8px 0 28px;\n background: var(--bg-base);\n border: 1px solid var(--border-soft);\n border-radius: var(--radius-sm);\n color: var(--fg);\n font-family: inherit;\n font-size: 11.5px;\n}\n.tk-filterbar select {\n height: 28px;\n font-size: 11.5px;\n padding: 0 var(--space-2);\n background: var(--bg-base);\n border: 1px solid var(--border-soft);\n border-radius: var(--radius-sm);\n color: var(--fg);\n font-family: inherit;\n}\n.tk-filterbar .label {\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--fg-faint);\n letter-spacing: 0.06em;\n text-transform: uppercase;\n}\n.tk-filterbar .spacer { flex: 1; }\n.tk-toggle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 28px;\n padding: 0 10px;\n background: var(--bg-base);\n border: 1px solid var(--border-soft);\n border-radius: var(--radius-sm);\n color: var(--fg-muted);\n font-family: inherit;\n font-size: 11px;\n cursor: pointer;\n}\n.tk-toggle:hover { color: var(--fg); border-color: var(--fg-faint); }\n.tk-toggle.is-on {\n color: var(--live);\n background: var(--live-soft);\n border-color: color-mix(in srgb, var(--live) 30%, var(--border));\n}\n.tk-toggle .checkbox {\n width: 11px;\n height: 11px;\n border: 1px solid currentColor;\n border-radius: 2px;\n display: inline-grid;\n place-items: center;\n flex-shrink: 0;\n}\n.tk-toggle.is-on .checkbox::after {\n content: '✓';\n font-size: 9px;\n line-height: 1;\n color: currentColor;\n}\n\n/* === Master/detail split ==================================== */\n.tk-split {\n --list-w: 360px;\n display: grid;\n grid-template-columns: minmax(280px, var(--list-w)) 4px minmax(420px, 1fr);\n /* Bind the single row to the split's own height. Without this the implicit\n * row is `auto` (content-sized), so a tall list/detail grows the row past the\n * split and gets clipped by overflow:hidden — neither pane scrolls. */\n grid-template-rows: minmax(0, 1fr);\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n.tk-list {\n overflow-y: auto;\n /* Grid items default to min-height:auto (won't shrink below content), which\n * defeats overflow:auto. Allow shrink so the list scrolls within its track. */\n min-height: 0;\n background: var(--bg-surface);\n}\n.tk-divider {\n background: var(--border-soft);\n cursor: col-resize;\n}\n.tk-divider:hover { background: var(--tint-fg-active, var(--primary)); }\n.tk-detail {\n background: var(--bg-base);\n overflow-y: auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* === Group dividers ========================================= */\n.tk-group-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px var(--space-4) 6px var(--space-3);\n background: var(--bg-sunken);\n border-bottom: 1px solid var(--border-soft);\n border-top: 1px solid var(--border-soft);\n font-family: var(--font-mono);\n font-size: 9.5px;\n color: var(--fg-faint);\n letter-spacing: 0.1em;\n text-transform: uppercase;\n position: sticky;\n top: 0;\n z-index: 4;\n cursor: pointer;\n user-select: none;\n}\n.tk-group-head:first-child { border-top: 0; }\n.tk-group-head .ct {\n color: var(--fg-muted);\n font-variant-numeric: tabular-nums;\n}\n.tk-group-head.is-overdue { color: var(--danger); }\n.tk-group-head.is-overdue .ct { color: var(--danger); }\n.tk-group-head.is-autoclosed { color: var(--live); }\n.tk-group-head.is-autoclosed .ct { color: var(--live); }\n.tk-group-head.is-collapsed { background: var(--bg-base); }\n.tk-group-head .chev {\n width: 10px;\n height: 10px;\n color: var(--fg-faint);\n transition: transform var(--motion-fast) var(--ease-calm);\n}\n.tk-group-head.is-collapsed .chev { transform: rotate(-90deg); }\n.tk-group-label {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n/* === Task row =============================================== */\n.tk-row {\n display: grid;\n grid-template-columns: 14px 1fr auto;\n gap: var(--space-3);\n padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--border-soft);\n cursor: pointer;\n position: relative;\n background: transparent;\n text-align: left;\n width: 100%;\n border-left: 0;\n border-right: 0;\n border-top: 0;\n font: inherit;\n color: inherit;\n transition: background var(--motion-fast) var(--ease-calm);\n}\n.tk-row:hover { background: var(--bg-raised); }\n.tk-row.is-on { background: var(--bg-raised); }\n.tk-row.is-on::before {\n content: '';\n position: absolute;\n left: 0;\n top: 8px;\n bottom: 8px;\n width: 2px;\n border-radius: 2px;\n background: var(--tint-fg-active, var(--primary));\n}\n.tk-row .pri-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n margin-top: 5px;\n background: var(--fg-faint);\n flex-shrink: 0;\n}\n.tk-row .pri-dot.is-critical {\n background: var(--danger);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--danger) 20%, transparent);\n}\n.tk-row .pri-dot.is-high { background: var(--achievement); }\n.tk-row .pri-dot.is-medium { background: var(--systemic); }\n.tk-row .pri-dot.is-low { background: var(--fg-faint); }\n.tk-row .body {\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.tk-row .title {\n font-size: var(--text-body-sm);\n color: var(--fg);\n font-weight: 500;\n line-height: 1.35;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n.tk-row.is-completed .title {\n color: var(--fg-muted);\n text-decoration: line-through;\n text-decoration-thickness: 1px;\n}\n.tk-row .meta {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n font-family: var(--font-mono);\n font-size: 9.5px;\n color: var(--fg-faint);\n letter-spacing: 0.04em;\n}\n.tk-row .meta .cat {\n background: var(--bg-base);\n border: 1px solid var(--border-soft);\n color: var(--fg-muted);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n text-transform: lowercase;\n}\n.tk-row .right {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 4px;\n flex-shrink: 0;\n}\n.tk-row .due {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg-muted);\n letter-spacing: 0.04em;\n white-space: nowrap;\n}\n.tk-row .due.is-overdue { color: var(--danger); font-weight: 500; }\n.tk-row .due.is-today { color: var(--achievement); }\n\n/* === Status badge =========================================== */\n.tk-badge {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-family: var(--font-mono);\n font-size: 9.5px;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: var(--radius-xs);\n border: 1px solid var(--border-soft);\n color: var(--fg-muted);\n background: var(--bg-base);\n white-space: nowrap;\n}\n.tk-badge.is-pending { color: var(--fg-muted); }\n.tk-badge.is-in_progress {\n color: var(--live);\n background: var(--live-soft);\n border-color: color-mix(in srgb, var(--live) 30%, var(--border-soft));\n}\n.tk-badge.is-blocked {\n color: var(--danger);\n background: color-mix(in srgb, var(--danger) 12%, transparent);\n border-color: color-mix(in srgb, var(--danger) 30%, var(--border-soft));\n}\n.tk-badge.is-completed {\n color: var(--live);\n background: var(--live-soft);\n border-color: color-mix(in srgb, var(--live) 30%, var(--border-soft));\n}\n.tk-badge.is-cancelled { color: var(--fg-faint); }\n.tk-badge .dot {\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background: currentColor;\n}\n\n/* === Auto-close badge ======================================= */\n.tk-autoclose {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.06em;\n color: var(--live);\n background: var(--live-soft);\n border: 1px solid color-mix(in srgb, var(--live) 25%, var(--border-soft));\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n white-space: nowrap;\n}\n.tk-autoclose svg { width: 9px; height: 9px; }\n\n/* === Assignee chips ========================================= */\n.tk-assignee {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-family: var(--font-mono);\n font-size: 9.5px;\n letter-spacing: 0.04em;\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n border: 1px solid var(--border-soft);\n background: var(--bg-base);\n color: var(--fg-muted);\n white-space: nowrap;\n}\n.tk-assignee.is-agent {\n color: var(--agent);\n background: var(--agent-soft);\n border-color: color-mix(in srgb, var(--agent) 30%, var(--border-soft));\n}\n.tk-assignee .avatar {\n width: 11px;\n height: 11px;\n border-radius: 50%;\n background: var(--bg-raised);\n color: var(--fg);\n display: inline-grid;\n place-items: center;\n font-size: 7.5px;\n font-weight: 600;\n text-transform: uppercase;\n}\n.tk-assignee.is-agent .avatar {\n background: color-mix(in srgb, var(--agent) 24%, transparent);\n color: var(--agent);\n}\n.tk-assignee .dot {\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background: var(--agent);\n}\n\n/* === Exec mode pill ========================================= */\n.tk-execmode {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: var(--fg-faint);\n border: 1px dashed var(--border-soft);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n background: transparent;\n}\n.tk-execmode.is-autonomous {\n color: var(--agent);\n border-color: color-mix(in srgb, var(--agent) 30%, var(--border-soft));\n}\n.tk-execmode.is-approval_required {\n color: var(--achievement);\n border-color: color-mix(in srgb, var(--achievement) 35%, var(--border-soft));\n}\n.tk-execmode.is-report {\n color: var(--systemic);\n border-color: color-mix(in srgb, var(--systemic) 30%, var(--border-soft));\n}\n\n/* === Detail panel =========================================== */\n.tk-det-head {\n padding: var(--space-4) var(--space-5);\n border-bottom: 1px solid var(--border-soft);\n background: linear-gradient(180deg, var(--tint-bg-active, var(--bg-surface)) 0%, var(--bg-base) 100%);\n}\n.tk-det-topline {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-3);\n margin-bottom: 8px;\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--fg-faint);\n letter-spacing: 0.04em;\n}\n.tk-det-topline .id {\n background: var(--bg-base);\n border: 1px solid var(--border-soft);\n padding: 2px 6px;\n border-radius: var(--radius-xs);\n color: var(--fg);\n}\n.tk-det-actions { display: flex; gap: 4px; align-items: center; }\n.tk-det-title {\n font-family: var(--font-display);\n font-weight: 500;\n font-size: var(--text-h3);\n margin: 0;\n color: var(--fg);\n line-height: 1.25;\n}\n.tk-det-meta-row {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-2);\n margin-top: var(--space-3);\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--fg-faint);\n letter-spacing: 0.04em;\n}\n.tk-det-meta-row .sep { color: var(--fg-faint); }\n.tk-det-meta-row .pri-label { color: var(--achievement); display: inline-flex; align-items: center; gap: 4px; }\n.tk-det-meta-row .pri-label.is-critical { color: var(--danger); }\n.tk-det-meta-row .pri-label.is-high { color: var(--achievement); }\n.tk-det-meta-row .pri-label.is-medium { color: var(--systemic); }\n.tk-det-meta-row .pri-label.is-low { color: var(--fg-muted); }\n.tk-det-meta-row .pri-label .dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: currentColor;\n}\n.tk-det-meta-row .due-text { color: var(--fg-muted); }\n.tk-det-meta-row .due-text.is-overdue { color: var(--danger); }\n.tk-det-body {\n padding: var(--space-5);\n display: flex;\n flex-direction: column;\n gap: var(--space-5);\n flex: 1;\n}\n.tk-det-grid {\n display: grid;\n grid-template-columns: 110px 1fr;\n gap: 10px var(--space-4);\n font-size: var(--text-body-sm);\n}\n.tk-det-grid dt {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg-faint);\n letter-spacing: 0.1em;\n text-transform: uppercase;\n align-self: center;\n}\n.tk-det-grid dd {\n margin: 0;\n color: var(--fg);\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-2);\n font-size: var(--text-body-sm);\n}\n.tk-det-grid dd code {\n font-family: var(--font-mono);\n font-size: 11.5px;\n background: var(--bg-sunken);\n border: 1px solid var(--border-soft);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n color: var(--fg);\n}\n.tk-section-label {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--fg-faint);\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n margin-bottom: 6px;\n}\n.tk-section-label .ct {\n font-family: var(--font-mono);\n font-size: 9.5px;\n color: var(--fg-muted);\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.04em;\n text-transform: none;\n}\n.tk-deferred-pill {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: var(--fg-faint);\n border: 1px dashed var(--border);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n background: transparent;\n}\n\n.tk-desc {\n font-size: var(--text-body-sm);\n color: var(--fg-muted);\n line-height: 1.6;\n background: var(--bg-surface);\n border: 1px solid var(--border-soft);\n border-radius: var(--radius-md);\n padding: var(--space-3) var(--space-4);\n white-space: pre-wrap;\n}\n\n/* === Progress bar =========================================== */\n.tk-progress {\n flex: 1;\n height: 6px;\n background: var(--bg-sunken);\n border-radius: 3px;\n overflow: hidden;\n border: 1px solid var(--border-soft);\n min-width: 120px;\n}\n.tk-progress > span {\n display: block;\n height: 100%;\n background: var(--live);\n border-radius: 3px;\n transition: width var(--motion-fast) var(--ease-calm);\n}\n\n/* === Evidence card ========================================== */\n.tk-evidence {\n border: 1px solid color-mix(in srgb, var(--live) 30%, var(--border-soft));\n background: var(--live-soft);\n border-radius: var(--radius-md);\n padding: var(--space-3) var(--space-4);\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n.tk-evidence-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-3);\n}\n.tk-evidence .rule-chip {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-family: var(--font-mono);\n font-size: 9.5px;\n letter-spacing: 0.04em;\n color: var(--live);\n background: var(--bg-surface);\n border: 1px solid color-mix(in srgb, var(--live) 30%, var(--border-soft));\n padding: 2px 6px;\n border-radius: var(--radius-xs);\n}\n.tk-evidence .rule-chip svg { width: 10px; height: 10px; }\n.tk-evidence .rule-chip.is-flag {\n color: var(--achievement);\n border-color: color-mix(in srgb, var(--achievement) 30%, var(--border-soft));\n background: var(--bg-surface);\n}\n.tk-evidence .timestamp {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg-muted);\n letter-spacing: 0.04em;\n}\n.tk-evidence .body {\n font-size: var(--text-body-sm);\n color: var(--fg);\n line-height: 1.55;\n}\n\n/* === Source-ref chips ======================================= */\n.tk-source-row {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n.tk-src {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--fg-muted);\n background: var(--bg-sunken);\n border: 1px solid var(--border-soft);\n padding: 3px 7px;\n border-radius: var(--radius-sm);\n text-decoration: none;\n letter-spacing: 0.02em;\n cursor: pointer;\n}\n.tk-src:hover { color: var(--fg); border-color: var(--fg-faint); }\n.tk-src svg { width: 11px; height: 11px; flex-shrink: 0; color: var(--fg-faint); }\n.tk-src.is-email svg { color: var(--mail-fg, var(--achievement)); }\n.tk-src.is-session svg { color: var(--agent); }\n.tk-src.is-git svg { color: var(--systemic); }\n\n/* === Subtasks =============================================== */\n.tk-subtasks { display: flex; flex-direction: column; gap: 4px; }\n.tk-sub-row {\n display: grid;\n grid-template-columns: auto 1fr auto;\n align-items: center;\n gap: var(--space-2);\n padding: 6px var(--space-3);\n background: var(--bg-surface);\n border: 1px solid var(--border-soft);\n border-radius: var(--radius-sm);\n font-size: var(--text-body-sm);\n cursor: pointer;\n text-align: left;\n width: 100%;\n font: inherit;\n color: inherit;\n transition: background var(--motion-fast) var(--ease-calm);\n}\n.tk-sub-row:hover { background: var(--bg-raised); }\n.tk-sub-row .name {\n color: var(--fg);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.tk-sub-row.is-completed .name {\n color: var(--fg-muted);\n text-decoration: line-through;\n}\n.tk-sub-row .due {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg-faint);\n}\n\n/* === Activity timeline ====================================== */\n.tk-timeline {\n display: flex;\n flex-direction: column;\n gap: 0;\n border-left: 1px dashed var(--border);\n margin-left: 6px;\n padding-left: var(--space-4);\n position: relative;\n}\n.tk-tl-item {\n position: relative;\n padding: 6px 0;\n font-size: var(--text-body-sm);\n color: var(--fg-muted);\n}\n.tk-tl-item::before {\n content: '';\n position: absolute;\n left: calc(-1 * var(--space-4) - 4px);\n top: 12px;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--bg-sunken);\n border: 1px solid var(--border);\n}\n.tk-tl-item.is-mark::before {\n background: var(--tint-fg-active, var(--primary));\n border-color: var(--tint-fg-active, var(--primary));\n}\n.tk-tl-item.is-ok::before {\n background: var(--live);\n border-color: var(--live);\n}\n.tk-tl-item .when {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg-faint);\n letter-spacing: 0.04em;\n margin-right: 6px;\n}\n.tk-tl-item .actor {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg);\n background: var(--bg-sunken);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n border: 1px solid var(--border-soft);\n margin-right: 6px;\n}\n.tk-tl-item .actor.is-agent { color: var(--agent); }\n\n/* === Action footer ========================================== */\n.tk-action-bar {\n border-top: 1px solid var(--border-soft);\n background: var(--bg-sunken);\n padding: var(--space-3) var(--space-5);\n display: flex;\n align-items: center;\n gap: var(--space-2);\n margin-top: auto;\n}\n.tk-action-bar .spacer { flex: 1; }\n\n/* === Empty state in detail =================================== */\n.tk-empty {\n flex: 1;\n display: grid;\n place-items: center;\n color: var(--fg-faint);\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n}\n\n/* === Density variants for $taskId standalone (Section B) ===== */\n.tk-detail-pane.is-compact .tk-det-head { padding: var(--space-3) var(--space-4); }\n.tk-detail-pane.is-compact .tk-det-title { font-size: var(--text-h4); }\n.tk-detail-pane.is-compact .tk-det-body { padding: var(--space-4); gap: var(--space-4); }\n.tk-detail-pane.is-compact .tk-det-grid { grid-template-columns: 90px 1fr; }\n.tk-detail-pane.is-side .tk-det-head { padding: var(--space-3); }\n.tk-detail-pane.is-side .tk-det-title { font-size: 13.5px; line-height: 1.35; }\n.tk-detail-pane.is-side .tk-det-body { padding: var(--space-3); gap: var(--space-3); }\n.tk-detail-pane.is-side .tk-det-grid { display: none; }\n.tk-detail-pane.is-side .tk-section-label { margin-bottom: 4px; }\n\n/* === In-body view switcher (D-2 / D-3) ====================== */\n.ip-tabs {\n display: flex;\n align-items: center;\n gap: 0;\n padding: 0 var(--space-3);\n height: var(--tab-h, 38px);\n border-bottom: 1px solid var(--border-soft);\n background: var(--bg-sunken);\n}\n.ip-tab {\n height: var(--tab-h, 38px);\n padding: 0 var(--space-3);\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: var(--font-body);\n font-size: var(--text-body-sm);\n color: var(--fg-faint);\n border: 0;\n background: transparent;\n cursor: pointer;\n border-bottom: 2px solid transparent;\n margin-bottom: -1px;\n}\n.ip-tab svg { width: 13px; height: 13px; opacity: 0.8; }\n.ip-tab:hover { color: var(--fg-muted); }\n.ip-tab.is-on { color: var(--fg); border-bottom-color: var(--tint-fg-active, var(--primary)); }\n.ip-tab.is-on svg { opacity: 1; color: var(--tint-fg-active, var(--primary)); }\n.ip-tab:focus-visible,\n.tk-toggle:focus-visible,\n.tk-group-head:focus-visible,\n.tk-row:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }\n.ip-tab-badge {\n font-family: var(--font-mono);\n font-size: 9.5px;\n line-height: 1;\n color: var(--achievement);\n background: var(--achievement-soft);\n border: 1px solid color-mix(in srgb, var(--achievement) 30%, var(--border-soft));\n border-radius: var(--radius-pill, 999px);\n padding: 2px 5px;\n}\n\n/* === Agenda / Today (D-2) =================================== */\n/* AgendaView root — rendered directly inside .tk-frame (which is overflow:hidden),\n * so it must be the scroll region itself: grow to fill the leftover height, allow\n * shrink (min-height:0), scroll its own overflow. Mirrors the Browse page's\n * `1fr` + overflow-y:auto leaf (shell .ccfg-list/.ccfg-detail). Without this the\n * agenda is clipped by the frame and can't scroll. */\n.ag-wrap { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-5); max-width: 920px; }\n.ag-head {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--space-3);\n margin-bottom: var(--space-5);\n}\n.ag-date { font-family: var(--font-display); font-weight: 500; font-size: var(--text-h3); color: var(--fg); }\n.ag-summary { font-family: var(--font-mono); font-size: 10.5px; color: var(--fg-faint); letter-spacing: 0.04em; }\n.ag-summary b { color: var(--fg-muted); font-weight: 500; }\n.ag-filter-note { color: var(--achievement); font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.04em; }\n.ag-grid { display: grid; grid-template-columns: 54px 1fr; column-gap: var(--space-3); }\n.ag-time { font-family: var(--font-mono); font-size: 10px; color: var(--fg-faint); text-align: right; padding-top: 8px; letter-spacing: 0.04em; }\n.ag-lane { min-width: 0; padding-bottom: var(--space-3); }\n.ag-block {\n padding: 8px var(--space-3);\n border-radius: var(--radius-sm);\n border: 1px solid var(--border-soft);\n background: var(--bg-surface);\n border-left: 3px solid var(--fg-faint);\n margin-bottom: 6px;\n}\n.ag-block:last-child { margin-bottom: 0; }\n.ag-block.is-me { border-left-color: var(--primary); }\n.ag-block.is-agent { border-left-color: var(--agent); background: var(--agent-soft); }\n.ag-block.is-silent { border-left-color: var(--systemic); border-style: dashed; background: transparent; }\n.ag-block.is-deadline { border-left-color: var(--danger); background: color-mix(in srgb, var(--danger) 8%, transparent); }\n.ag-block.is-done { opacity: 0.6; }\n.ag-block .t { font-size: var(--text-body-sm); color: var(--fg); font-weight: 500; line-height: 1.4; }\n.ag-block.is-done .t { text-decoration: line-through; text-decoration-thickness: 1px; color: var(--fg-muted); }\n.ag-block .m { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 5px; }\n.ag-now { grid-column: 1 / -1; position: relative; height: 0; border-top: 2px solid var(--live); margin: 4px 0 10px; }\n.ag-now::after {\n content: 'now';\n position: absolute;\n right: 0;\n top: -8px;\n font-family: var(--font-mono);\n font-size: 8.5px;\n letter-spacing: 0.06em;\n color: var(--live);\n background: var(--bg-base);\n padding: 0 5px;\n}\n.ag-now::before { content: ''; position: absolute; left: -5px; top: -3px; width: 6px; height: 6px; border-radius: 50%; background: var(--live); }\n\n/* === Triage / Health (D-3) ================================== */\n/* TriageView root — same as .ag-wrap: own scroll region inside the overflow-hidden\n * frame (grow to fill, min-height:0, scroll). */\n.tr-wrap { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-5); max-width: 980px; }\n.tr-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-3); margin-bottom: var(--space-5); }\n.tr-stat { padding: var(--space-3) var(--space-4); border: 1px solid var(--border-soft); border-radius: var(--radius-md); background: var(--bg-surface); }\n/* Numbers stay --fg (12:1+ in every theme/mode) — the colored border carries\n the semantic. --achievement big-number measured 2.57:1 on a light surface. */\n.tr-stat .n { font-family: var(--font-display); font-weight: 500; font-size: 28px; line-height: 1; color: var(--fg); font-variant-numeric: tabular-nums; }\n.tr-stat .k { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--fg-faint); margin-top: 8px; }\n.tr-stat .sub { font-family: var(--font-mono); font-size: 9px; color: var(--fg-faint); letter-spacing: 0.02em; margin-top: 3px; }\n.tr-stat.is-danger { border-color: color-mix(in srgb, var(--danger) 35%, var(--border-soft)); }\n.tr-stat.is-warn { border-color: color-mix(in srgb, var(--achievement) 35%, var(--border-soft)); }\n.tr-stat.is-sys { border-color: color-mix(in srgb, var(--systemic) 35%, var(--border-soft)); }\n.tr-bucket { margin-bottom: var(--space-5); }\n.tr-bucket-head {\n display: flex;\n align-items: center;\n gap: var(--space-2);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n color: var(--fg-faint);\n margin-bottom: var(--space-2);\n}\n.tr-bucket-head .ct { color: var(--fg-muted); font-variant-numeric: tabular-nums; }\n.tr-bucket-head::after { content: ''; flex: 1; height: 1px; background: var(--border-soft); }\n.tr-mini {\n display: grid;\n grid-template-columns: auto 1fr auto auto;\n align-items: center;\n gap: var(--space-3);\n padding: 8px var(--space-3);\n border-bottom: 1px solid var(--border-soft);\n}\n.tr-mini:last-child { border-bottom: 0; }\n.tr-mini .title { font-size: var(--text-body-sm); color: var(--fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }\n.tr-mini .age { font-family: var(--font-mono); font-size: 10px; color: var(--fg-muted); letter-spacing: 0.04em; white-space: nowrap; }\n.tr-mini .age.is-bad { color: var(--danger); }\n.tr-more { font-family: var(--font-mono); font-size: 10px; color: var(--fg-faint); letter-spacing: 0.04em; padding: 6px var(--space-3); }\n.tr-health {\n display: flex;\n align-items: center;\n gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border: 1px dashed var(--border);\n border-radius: var(--radius-md);\n background: var(--bg-sunken);\n font-size: var(--text-body-sm);\n color: var(--fg-muted);\n line-height: 1.55;\n}\n.tr-health .score { font-family: var(--font-display); font-weight: 500; font-size: var(--text-h2); color: var(--achievement); line-height: 1; flex-shrink: 0; }\n\n/* === De-Tailwinded utilities ================================== */\n/* The source app leaned on a handful of Tailwind utility classes for layout\n * chrome around the locked .tk-* / .ag-* / .tr-* visuals. With no Tailwind in\n * the no-build pkg, those are reproduced here as a tiny, scoped utility set\n * (named after their Tailwind origin so the htm transcription reads 1:1).\n * Everything load-bearing is still a .tk-, .ag-, .tr- rule above. */\n\n/* Root layout shell (was: `flex h-full flex-col p-5`) */\n.tk-screen {\n display: flex;\n height: 100%;\n flex-direction: column;\n}\n\n/* Loading / error / empty inline states (was Tailwind utility soup) */\n.tk-loading {\n display: flex;\n align-items: center;\n gap: var(--space-2);\n padding: var(--space-4);\n font-size: var(--text-body-sm);\n color: var(--fg-muted);\n}\n.tk-error {\n margin: var(--space-4);\n display: flex;\n align-items: flex-start;\n gap: var(--space-2);\n border-radius: var(--radius-md);\n border: 1px solid color-mix(in srgb, var(--danger) 50%, transparent);\n background: color-mix(in srgb, var(--danger) 10%, transparent);\n padding: var(--space-3);\n font-size: var(--text-body-sm);\n color: var(--danger);\n}\n.tk-error svg { flex-shrink: 0; margin-top: 2px; }\n.tk-error .t { font-weight: 500; }\n.tk-error .d { font-size: var(--text-caption); opacity: 0.8; margin-top: 2px; }\n.tk-empty-box {\n margin: var(--space-4);\n display: flex;\n height: 128px;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-md);\n border: 1px dashed var(--border);\n font-size: var(--text-body-sm);\n color: var(--fg-muted);\n}\n\n/* Spinner (was: `animate-spin`) */\n.tk-spin { animation: tk-spin 1s linear infinite; }\n@keyframes tk-spin { to { transform: rotate(360deg); } }\n\n/* Detail-pane status select inline-styled in source; kept inline in the htm. */\n\n/* === Button (was: src/components/Button.tsx, Tailwind utilities) ============ */\n.tk-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n font-family: inherit;\n font-weight: 500;\n white-space: nowrap;\n cursor: pointer;\n transition: background var(--motion-fast, 120ms) var(--ease-calm, ease),\n color var(--motion-fast, 120ms) var(--ease-calm, ease),\n opacity var(--motion-fast, 120ms) var(--ease-calm, ease);\n}\n.tk-btn svg { flex-shrink: 0; }\n.tk-btn:disabled { opacity: 0.5; pointer-events: none; }\n/* sizes */\n.tk-btn.sz-md { height: 32px; padding: 0 12px; font-size: var(--text-body-sm); border-radius: var(--radius-md); }\n.tk-btn.sz-sm { height: 28px; padding: 0 10px; font-size: var(--text-caption); border-radius: var(--radius-md); gap: 4px; }\n/* variants */\n.tk-btn.v-default {\n background: var(--primary);\n color: var(--primary-fg);\n border: 1px solid transparent;\n}\n.tk-btn.v-default:hover:not(:disabled) { opacity: 0.9; }\n.tk-btn.v-outline {\n background: transparent;\n color: var(--fg);\n border: 1px solid var(--border);\n}\n.tk-btn.v-outline:hover:not(:disabled) { background: var(--bg-raised); }\n.tk-btn.v-ghost {\n background: transparent;\n color: var(--fg);\n border: 1px solid transparent;\n}\n.tk-btn.v-ghost:hover:not(:disabled) { background: var(--bg-raised); }\n.tk-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }\n\n/* Mutation error line in the detail pane footer. */\n.tk-mut-error {\n padding: 0 var(--space-5) var(--space-3);\n font-size: 11px;\n color: var(--danger);\n}\n";
package/dist/lib/ui.js CHANGED
@@ -57,6 +57,11 @@ const ICONS = {
57
57
  mail: 'M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zM22 6l-10 7L2 6',
58
58
  terminal: 'M4 17l6-6-6-6M12 19h8',
59
59
  'git-branch': 'M6 3v12M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM18 9a9 9 0 0 1-9 9',
60
+ // ViewTabs glyphs for Sweeper + Done. `broom` mirrors the design's SWEEP
61
+ // glyph (a broom strokes + handle); `check-check` is the lucide name for
62
+ // the "double check" used for completed items.
63
+ broom: 'M9.8 12.2 5 17l3 3 4.8-4.8M14 8l-3.5 3.5 4 4L18 12l-4-4z M14 8l5-5M19 3l2 2',
64
+ 'check-check': 'M18 6 7 17l-5-5M22 10l-7.5 7.5L13 16',
60
65
  };
61
66
 
62
67
  // Class-name joiner — replaces clsx + tailwind-merge. With Tailwind gone there
package/dist/tasks.css CHANGED
@@ -4,30 +4,32 @@
4
4
 
5
5
  /* === Frame ================================================== */
6
6
  .tk-frame {
7
- border: 1px solid var(--border);
8
- border-radius: var(--radius-lg);
7
+ /* No card chrome inside the shell — the pkg pane IS the container, so the
8
+ * content fills it flush (mirrors the design's `#tk-host > .frame` reset).
9
+ * No border / radius / shadow / surrounding padding. */
9
10
  background: var(--bg-surface);
10
11
  overflow: hidden;
11
- box-shadow: var(--shadow-2);
12
12
  display: flex;
13
13
  flex-direction: column;
14
14
  min-height: 0;
15
15
  }
16
16
  .tk-frame-head {
17
17
  display: flex;
18
- align-items: flex-start;
18
+ align-items: center;
19
19
  justify-content: space-between;
20
20
  gap: var(--space-4);
21
- padding: var(--space-4) var(--space-5);
21
+ /* Slim single-row action bar (R-header): the sidebar carries domain + view
22
+ * nav, so this bar holds only the active-view label + the New task action. */
23
+ padding: var(--space-2) var(--space-5);
22
24
  border-bottom: 1px solid var(--border-soft);
23
25
  background: linear-gradient(180deg, var(--tint-bg-active, var(--bg-surface)) 0%, var(--bg-surface) 100%);
24
26
  }
25
27
  .tk-frame-title-wrap { display: flex; align-items: center; gap: var(--space-2); }
26
- .tk-frame-title-mark { width: 18px; height: 18px; color: var(--tint-fg-active, var(--primary)); }
28
+ .tk-frame-title-mark { width: 15px; height: 15px; color: var(--tint-fg-active, var(--primary)); }
27
29
  .tk-frame-title {
28
30
  font-family: var(--font-display);
29
31
  font-weight: 500;
30
- font-size: var(--text-h3);
32
+ font-size: var(--text-body);
31
33
  margin: 0;
32
34
  color: var(--fg);
33
35
  }
@@ -946,14 +948,13 @@
946
948
  * chrome around the locked .tk-* / .ag-* / .tr-* visuals. With no Tailwind in
947
949
  * the no-build pkg, those are reproduced here as a tiny, scoped utility set
948
950
  * (named after their Tailwind origin so the htm transcription reads 1:1).
949
- * Everything load-bearing is still a .tk-*/.ag-*/.tr-* rule above. */
951
+ * Everything load-bearing is still a .tk-, .ag-, .tr- rule above. */
950
952
 
951
953
  /* Root layout shell (was: `flex h-full flex-col p-5`) */
952
954
  .tk-screen {
953
955
  display: flex;
954
956
  height: 100%;
955
957
  flex-direction: column;
956
- padding: var(--space-5);
957
958
  }
958
959
 
959
960
  /* Loading / error / empty inline states (was Tailwind utility soup) */
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "com.ikenga.tasks",
3
3
  "name": "Tasks",
4
- "version": "0.2.0",
4
+ "version": "0.8.0",
5
5
  "ikenga_api": "1",
6
6
  "kind": "embedded",
7
7
  "author": { "name": "Royalti", "key": "royalti" },
@@ -27,27 +27,20 @@
27
27
  "script-src": ["'self'", "'unsafe-inline'", "https://esm.sh"],
28
28
  "style-src": ["'self'", "'unsafe-inline'", "https://esm.sh"],
29
29
  "font-src": ["'self'", "https://esm.sh", "data:"],
30
- "connect-src": [
31
- "'self'",
32
- "https://esm.sh",
33
- "https://*.supabase.co",
34
- "wss://*.supabase.co"
35
- ]
30
+ "connect-src": ["'self'", "https://esm.sh"]
36
31
  }
37
32
  },
38
33
 
39
34
  "capabilities": {
40
- "sqlite": { "db": "ikenga.local" },
41
- "supabase": { "required": true }
35
+ "sqlite": { "db": "ikenga.local" }
42
36
  },
43
37
 
44
38
  "permissions": {
45
39
  "shell.execute": [],
46
40
  "fs.read": [],
47
41
  "fs.write": [],
48
- "net": ["https://esm.sh", "https://*.supabase.co"],
42
+ "net": ["https://esm.sh"],
49
43
  "sqlite.tables": ["tasks"],
50
- "supabase.tables": ["tasks", "task_comments"],
51
44
  "vault.keys": []
52
45
  }
53
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikenga/pkg-tasks",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Tasks — List, Agenda, Triage over the production tasks schema. Multi-file iframe pkg, no build step.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -21,4 +21,4 @@
21
21
  "build": "echo 'no build — multi-file ESM pkg'",
22
22
  "typecheck": "tsc -p tsconfig.dev.json"
23
23
  }
24
- }
24
+ }