@jini-ai/chat 0.6.6 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +129 -1
  2. package/dist/core/accepted-input.d.ts +54 -0
  3. package/dist/core/accepted-input.js +165 -0
  4. package/dist/core/agent-plugin-chat-state.d.ts +43 -0
  5. package/dist/core/agent-plugin-chat-state.js +48 -0
  6. package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
  7. package/dist/core/assistant-client/messages.d.ts +1 -1
  8. package/dist/core/assistant-client/messages.js +2 -2
  9. package/dist/core/assistant-client/transport.d.ts +2 -2
  10. package/dist/core/assistant-client/transport.js +187 -98
  11. package/dist/core/assistant-content.d.ts +2 -1
  12. package/dist/core/assistant-content.js +9 -11
  13. package/dist/core/browser/page-actions.js +4 -2
  14. package/dist/core/composer-history.d.ts +16 -9
  15. package/dist/core/composer-history.js +28 -16
  16. package/dist/core/conversation-title.d.ts +3 -0
  17. package/dist/core/conversation-title.js +3 -0
  18. package/dist/core/durable-projection.js +20 -13
  19. package/dist/core/durable-runs/continuation.d.ts +4 -0
  20. package/dist/core/durable-runs/continuation.js +31 -6
  21. package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
  22. package/dist/core/durable-runs/ports.d.ts +6 -2
  23. package/dist/core/index.d.ts +2 -0
  24. package/dist/core/index.js +2 -0
  25. package/dist/core/messages.d.ts +2 -0
  26. package/dist/core/partial-json.d.ts +6 -7
  27. package/dist/core/partial-json.js +81 -47
  28. package/dist/core/persistence/title.js +4 -2
  29. package/dist/core/question-answer.d.ts +23 -0
  30. package/dist/core/question-answer.js +66 -0
  31. package/dist/core/question-form/format.d.ts +6 -6
  32. package/dist/core/question-form/format.js +16 -17
  33. package/dist/core/question-form/normalize.d.ts +18 -0
  34. package/dist/core/question-form/normalize.js +112 -122
  35. package/dist/core/question-form/scan.js +141 -203
  36. package/dist/core/run-activity.js +62 -49
  37. package/dist/core/run-events/entry.d.ts +10 -1
  38. package/dist/core/run-events/entry.js +117 -87
  39. package/dist/core/todos.d.ts +1 -1
  40. package/dist/core/todos.js +60 -85
  41. package/dist/core/tool-events.js +14 -12
  42. package/dist/core/transcript.js +62 -16
  43. package/dist/core/turn-usage.js +54 -32
  44. package/dist/core/user-text-redaction.js +3 -0
  45. package/dist/core/util/manifest.js +77 -82
  46. package/dist/core/util/markdown-context.js +51 -53
  47. package/dist/core/util/parser.js +136 -144
  48. package/dist/core/util/pointer.js +16 -16
  49. package/dist/core/util/strip.js +57 -67
  50. package/dist/core/util/validate.js +15 -17
  51. package/dist/embed-widget/browser/session-store.js +21 -12
  52. package/dist/embed-widget/transports/fetch-sse.js +108 -95
  53. package/dist/embed-widget/vanilla/index.js +81 -70
  54. package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
  55. package/dist/react/components/AttachmentPreviewModal.js +25 -1
  56. package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
  57. package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
  58. package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
  59. package/dist/react/components/ChatSettingsDialog.js +72 -0
  60. package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
  61. package/dist/react/components/ChatSettingsSegmented.js +11 -0
  62. package/dist/react/components/Composer.d.ts +5 -1
  63. package/dist/react/components/Composer.js +126 -82
  64. package/dist/react/components/ConversationList.js +25 -39
  65. package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
  66. package/dist/react/components/ConversationTitleEditor.js +54 -0
  67. package/dist/react/components/Icon.d.ts +1 -1
  68. package/dist/react/components/Icon.js +25 -45
  69. package/dist/react/components/Markdown.js +134 -123
  70. package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
  71. package/dist/react/components/McpUiSurfaceCard.js +30 -9
  72. package/dist/react/components/MessageList.d.ts +13 -13
  73. package/dist/react/components/MessageList.hooks.d.ts +6 -1
  74. package/dist/react/components/MessageList.hooks.js +19 -3
  75. package/dist/react/components/MessageList.js +34 -10
  76. package/dist/react/components/MessageRow.d.ts +1 -1
  77. package/dist/react/components/MessageRow.js +75 -47
  78. package/dist/react/components/MessageRow.usage.js +16 -11
  79. package/dist/react/components/MidRunUserMessage.js +16 -3
  80. package/dist/react/components/QuestionForm.js +148 -76
  81. package/dist/react/components/QuestionsPanel.js +1 -1
  82. package/dist/react/components/TodoCard.js +12 -6
  83. package/dist/react/components/ToolCallGroup.d.ts +4 -3
  84. package/dist/react/components/ToolCallGroup.js +7 -5
  85. package/dist/react/components/ToolCard.js +47 -40
  86. package/dist/react/components/composer-discovery.js +6 -1
  87. package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
  88. package/dist/react/components/mcp-ui-surface-state.js +23 -8
  89. package/dist/react/ext-event-renderer-registry.d.ts +8 -0
  90. package/dist/react/ext-event-renderer-registry.js +5 -2
  91. package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
  92. package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
  93. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
  94. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  95. package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
  96. package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
  97. package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
  98. package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
  99. package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
  100. package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
  101. package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
  102. package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
  103. package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
  104. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
  105. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
  106. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
  107. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
  108. package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
  109. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
  110. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
  111. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
  112. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
  113. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
  114. package/dist/react/features/chat-pane/model-options.d.ts +14 -0
  115. package/dist/react/features/chat-pane/model-options.js +45 -25
  116. package/dist/react/features/chat-pane/rules.d.ts +8 -9
  117. package/dist/react/features/chat-pane/rules.js +84 -46
  118. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  119. package/dist/react/features/chat-pane/styles.js +152 -36
  120. package/dist/react/features/chat-pane/types.d.ts +19 -9
  121. package/dist/react/hooks/assistant-chats.js +12 -5
  122. package/dist/react/hooks/composer-draft-cache.js +116 -118
  123. package/dist/react/hooks/composer-history-storage.js +5 -5
  124. package/dist/react/hooks/fab-position.js +7 -4
  125. package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
  126. package/dist/react/hooks/useAnsweredCards.js +38 -11
  127. package/dist/react/hooks/useArtifactStream.js +28 -22
  128. package/dist/react/hooks/useComposer.d.ts +20 -0
  129. package/dist/react/hooks/useComposer.js +22 -9
  130. package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
  131. package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
  132. package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
  133. package/dist/react/hooks/useConversation.d.ts +7 -0
  134. package/dist/react/hooks/useConversation.js +23 -10
  135. package/dist/react/hooks/useExtEventGroups.js +39 -39
  136. package/dist/react/hooks/useQuestionForms.js +21 -26
  137. package/dist/react/hooks/useRunStream.js +10 -2
  138. package/dist/react/index.d.ts +7 -1
  139. package/dist/react/index.js +9 -0
  140. package/dist/react/message-blocks.d.ts +6 -4
  141. package/dist/react/message-blocks.js +94 -72
  142. package/dist/react/slots.d.ts +2 -2
  143. package/dist/react/tool-call-display.d.ts +12 -6
  144. package/dist/react/tool-call-display.js +53 -24
  145. package/dist/server/durable-runs/recover.js +132 -80
  146. package/dist/server/run-finalizer/entry.js +25 -15
  147. package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
  148. package/dist/store/legacy/sqlite/conversations.js +69 -98
  149. package/dist/store/legacy/sqlite/messages.d.ts +10 -2
  150. package/dist/store/legacy/sqlite/messages.js +113 -71
  151. package/dist/store/runtime/chat-history-store.js +9 -3
  152. package/dist/store/runtime/durable-run-store.d.ts +2 -2
  153. package/dist/store/runtime/durable-run-store.js +165 -117
  154. package/dist/store/runtime/history-tools.js +118 -82
  155. package/dist/store/runtime/run-ledger.d.ts +4 -1
  156. package/dist/store/runtime/run-ledger.js +56 -16
  157. package/dist/store/sql/cursor.js +38 -18
  158. package/dist/store/sql/store.d.ts +18 -0
  159. package/dist/store/sql/store.js +46 -15
  160. package/package.json +18 -16
@@ -1,4 +1,6 @@
1
1
  import { configuredPickerModel, pickerModelOptions, isConcretePickerModel } from './model-options.js';
2
+ import { isTerminalRunStatus } from '../../../core/messages.js';
3
+ import { acceptedInputEvent } from '../../../core/accepted-input.js';
2
4
  function preferredOptionId(options) {
3
5
  return options?.find((option) => option.id === 'default')?.id ?? options?.[0]?.id;
4
6
  }
@@ -7,37 +9,43 @@ function validatedOptionId(options, requested, fallback) {
7
9
  return requested;
8
10
  return fallback;
9
11
  }
10
- export function defaultChatPaneSelection({ agent }) {
11
- // An unresolved native route requires an intentional concrete choice. Legacy hosts without
12
- // structured resolution retain their first-concrete-entry behavior.
13
- const model = configuredPickerModel(agent) || (agent.defaultModelResolution?.status === 'unresolved' ? undefined : pickerModelOptions(agent)[0]?.id);
14
- const reasoning = preferredOptionId(agent.reasoningOptions);
12
+ /** Builds a selection, leaving out the model/reasoning keys rather than setting them to `undefined`. */
13
+ function selectionOf({ agentId, model, reasoning }) {
15
14
  return {
16
- agentId: agent.id,
15
+ agentId,
17
16
  ...(model === undefined ? {} : { model }),
18
17
  ...(reasoning === undefined ? {} : { reasoning }),
19
18
  };
20
19
  }
20
+ function isSelectableAgent(agent) {
21
+ return agent.id !== 'gemini' && agent.available !== false;
22
+ }
23
+ /** A concrete custom model passes through when the agent accepts one; anything else must be on the picker list. */
24
+ function resolveRequestedModel({ agent, requested, fallback }) {
25
+ const acceptsCustom = agent.supportsCustomModel && agent.supportsConcreteModelSelection !== false;
26
+ if (requested && acceptsCustom && isConcretePickerModel(requested))
27
+ return requested;
28
+ return validatedOptionId(pickerModelOptions(agent), requested, fallback);
29
+ }
30
+ export function defaultChatPaneSelection({ agent }) {
31
+ // Legacy hosts without structured resolution retain their first-concrete-entry behavior.
32
+ // Use that fallback for unresolved native defaults too: an off-list alias must not strand
33
+ // a usable catalog. The picker still excludes aliases without concrete model evidence.
34
+ const model = configuredPickerModel(agent) || pickerModelOptions(agent)[0]?.id;
35
+ const reasoning = preferredOptionId(agent.reasoningOptions);
36
+ return selectionOf({ agentId: agent.id, model, reasoning });
37
+ }
21
38
  export function resolveChatPaneSelection({ agents, requested }) {
22
- const requestedAgent = agents.find((agent) => agent.id === requested.agentId && agent.id !== 'gemini' && agent.available !== false);
23
- const agent = requestedAgent ?? agents.find((candidate) => candidate.id !== 'gemini' && candidate.available !== false);
39
+ const requestedAgent = agents.find((agent) => agent.id === requested.agentId && isSelectableAgent(agent));
40
+ const agent = requestedAgent ?? agents.find(isSelectableAgent);
24
41
  if (!agent)
25
42
  return { agentId: '' };
26
43
  const defaults = defaultChatPaneSelection({ agent: agent });
27
44
  if (agent !== requestedAgent)
28
45
  return defaults;
29
- const model = requested.model
30
- && isConcretePickerModel(requested.model)
31
- && agent.supportsCustomModel
32
- && agent.supportsConcreteModelSelection !== false
33
- ? requested.model
34
- : validatedOptionId(pickerModelOptions(agent), requested.model, defaults.model);
46
+ const model = resolveRequestedModel({ agent, requested: requested.model, fallback: defaults.model });
35
47
  const reasoning = validatedOptionId(agent.reasoningOptions, requested.reasoning, defaults.reasoning);
36
- return {
37
- agentId: agent.id,
38
- ...(model === undefined ? {} : { model }),
39
- ...(reasoning === undefined ? {} : { reasoning }),
40
- };
48
+ return selectionOf({ agentId: agent.id, model, reasoning });
41
49
  }
42
50
  /**
43
51
  * Whether the API/BYOK execution path is genuinely usable right now, independent of the local CLI
@@ -73,15 +81,31 @@ export function isChatPaneFileDropEnabled({ canUpload, disabled, unavailable, up
73
81
  * @returns The first blocker found, or `null` when sending is allowed.
74
82
  */
75
83
  export function findChatPaneSendBlocker(state) {
76
- if (state.selectedAgent === undefined) {
77
- if (!state.apiModeConfigured)
78
- return 'no-agent-selected';
79
- }
80
- else if (state.selectedAgent.available === false) {
84
+ return findAgentSendBlocker(state) ?? findReadinessSendBlocker(state);
85
+ }
86
+ /**
87
+ * Whether a catalog-backed (or concrete-selection-refusing) agent lacks a concrete model to run.
88
+ * An agent that refuses concrete selection can never resolve one, whatever `model` says.
89
+ */
90
+ function isModelUnresolved({ agent, model }) {
91
+ const refusesConcrete = agent.supportsConcreteModelSelection === false;
92
+ if (!agent.modelCatalog && !refusesConcrete)
93
+ return false;
94
+ return refusesConcrete || !model || !isConcretePickerModel(model);
95
+ }
96
+ /** The agent/model half of {@link findChatPaneSendBlocker}: a configured API mode stands in for a missing CLI. */
97
+ function findAgentSendBlocker(state) {
98
+ const agent = state.selectedAgent;
99
+ if (agent === undefined)
100
+ return state.apiModeConfigured ? null : 'no-agent-selected';
101
+ if (agent.available === false)
81
102
  return 'agent-unavailable';
82
- }
83
- if (!state.apiModeConfigured && state.selectedAgent && (state.selectedAgent.modelCatalog || state.selectedAgent.supportsConcreteModelSelection === false) && (!state.model || !isConcretePickerModel(state.model) || state.selectedAgent.supportsConcreteModelSelection === false))
103
+ if (!state.apiModeConfigured && isModelUnresolved({ agent, model: state.model }))
84
104
  return 'model-unresolved';
105
+ return null;
106
+ }
107
+ /** The run/upload/working-directory half of {@link findChatPaneSendBlocker}, in priority order. */
108
+ function findReadinessSendBlocker(state) {
85
109
  if (state.isStreaming)
86
110
  return 'streaming';
87
111
  if (state.activeUploadCount > 0)
@@ -144,7 +168,7 @@ export function isChatPaneQueueableBlocker({ blocker }) {
144
168
  *
145
169
  * A surface is attributed to the newest call still open when it arrived — the same rule
146
170
  * `useExtEventGroups` uses to give a card its `call` — so an older call returning does not settle
147
- * it. Once its call returns (answered, expired, abandoned), typing is a next turn again.
171
+ * it. A terminal run, a later turn, a returned call or an accepted answer receipt closes it.
148
172
  *
149
173
  * @complexity O(e·c): e events in the newest message, c calls open at once (one or two in practice).
150
174
  */
@@ -153,32 +177,46 @@ export function isAwaitingTypedAnswer({ messages, toolName }) {
153
177
  }
154
178
  /**
155
179
  * The tool-use id of the question {@link isAwaitingTypedAnswer} sees waiting, or `null` — the newest
156
- * one when several are open. The id lets the pane tell one question from the next: a draft typed
157
- * for a question stays tied to THAT question after it closes (see `useChatPane`'s held answer).
180
+ * one when several are open. Only an active assistant run can hold a question open.
158
181
  *
159
182
  * @complexity O(e·c), as {@link isAwaitingTypedAnswer}.
160
183
  */
161
184
  export function findAwaitedTypedAnswerId({ messages, toolName }) {
162
185
  const newest = messages.at(-1);
163
- if (newest?.role !== 'assistant')
186
+ // MOVE: Missing tool_result is not evidence that a finished or unknown run still awaits input.
187
+ if (newest?.role !== 'assistant' || newest.runStatus === undefined || isTerminalRunStatus({ status: newest.runStatus }))
164
188
  return null;
165
189
  // Open call id -> whether it is the question tool. Insertion-ordered, so the last entry is the
166
190
  // newest call still open; a repeat id keeps its slot and only ORs in its name.
167
191
  const openCalls = new Map();
168
192
  const awaited = new Set();
193
+ const settled = new Set();
169
194
  for (const event of newest.events ?? []) {
170
- if (event.kind === 'tool_use')
171
- openCalls.set(event.id, openCalls.get(event.id) === true || event.name === toolName);
172
- if (event.kind === 'tool_result') {
173
- openCalls.delete(event.toolUseId);
174
- awaited.delete(event.toolUseId);
175
- }
176
- const owner = event.kind === 'ext' ? [...openCalls].at(-1) : undefined;
177
- if (owner?.[1] === true)
178
- awaited.add(owner[0]);
195
+ trackTypedAnswerEvent({ event, toolName, openCalls, awaited, settled });
179
196
  }
180
197
  return [...awaited].at(-1) ?? null;
181
198
  }
199
+ function trackOpenQuestionCall({ event, toolName, openCalls, settled }, _options = {}) {
200
+ if (event.kind === 'tool_use' && !settled.has(event.id))
201
+ openCalls.set(event.id, openCalls.get(event.id) === true || event.name === toolName);
202
+ }
203
+ /** Folds one event into {@link findAwaitedTypedAnswerId}'s open-call and awaited-question state. */
204
+ function trackTypedAnswerEvent({ event, toolName, openCalls, awaited, settled }) {
205
+ const receipt = acceptedInputEvent({ event });
206
+ const closedId = event.kind === 'tool_result' ? event.toolUseId : receipt?.data.toolUseId;
207
+ if (closedId !== undefined) {
208
+ settled.add(closedId);
209
+ openCalls.delete(closedId);
210
+ awaited.delete(closedId);
211
+ }
212
+ // MOVE: Receipts are user bubbles, never a new surface owned by another open question.
213
+ if (receipt !== null)
214
+ return;
215
+ trackOpenQuestionCall({ event, toolName, openCalls, settled });
216
+ const owner = event.kind === 'ext' ? [...openCalls].at(-1) : undefined;
217
+ if (owner?.[1] === true)
218
+ awaited.add(owner[0]);
219
+ }
182
220
  /**
183
221
  * Whether a composer send is an answer to a waiting question rather than a next turn: a run is
184
222
  * streaming ({@link isChatPaneQueueableBlocker}), the turn has no attachments (a typed answer
@@ -192,28 +230,28 @@ export function isTypedAnswerTurn({ blocker, attachmentCount, messages, toolName
192
230
  }
193
231
  /**
194
232
  * The run a composer send can go INTO instead of queueing behind, or `null`: a run is streaming
195
- * ({@link isChatPaneQueueableBlocker}), the turn has no attachments (a mid-run message carries text
196
- * only — the live agent's stdin takes no files), and the newest message is that run's assistant
233
+ * ({@link isChatPaneQueueableBlocker}), and the newest message is that run's assistant
197
234
  * turn with a known `runId`.
235
+ * Attachment support is decided by the host's delivery, never by dropping them here.
198
236
  *
199
237
  * @complexity O(1).
200
238
  */
201
- export function findMidRunMessageRunId({ blocker, attachmentCount, messages }) {
202
- if (!isChatPaneQueueableBlocker({ blocker }) || attachmentCount !== 0)
239
+ export function findMidRunMessageRunId({ blocker, messages }) {
240
+ if (!isChatPaneQueueableBlocker({ blocker }))
203
241
  return null;
204
242
  const last = messages.at(-1);
205
243
  return last?.role === 'assistant' && last.runId ? last.runId : null;
206
244
  }
207
245
  /**
208
- * The queued next turn after one more message joins it. The pane holds ONE queued turn, so a second
209
- * message sent behind the same run is appended to it (a blank line apart) rather than replacing it:
246
+ * MOVE: Display is per-message; delivery is ONE queued turn. A second message behind the same run
247
+ * is appended to that delivery (a blank line apart) rather than replacing it:
210
248
  * replacing silently dropped the first message (owner, 2026-10-10). One turn, not two, because both
211
249
  * were written for the run still going and the agent should read them together — two queued turns
212
250
  * would also start two paid runs back to back.
213
251
  *
214
252
  * @complexity O(n) in the combined text length.
215
253
  */
216
- export function appendQueuedTurn({ queued, next }) {
254
+ export function appendQueuedTurn({ queued, next }, _options = {}) {
217
255
  if (queued === null)
218
256
  return next;
219
257
  return {
@@ -1,2 +1,2 @@
1
- export declare const CHAT_PANE_STYLES = "\n.jini-chat-pane {\n --jini-chat-bg: #faf9f7;\n --jini-chat-panel: #fdfcfa;\n --jini-chat-text: #1a1916;\n --jini-chat-text-strong: #0d0c0a;\n --jini-chat-muted: #74716b;\n --jini-chat-faint: #b3b0a8;\n --jini-chat-border: #e1e5eb;\n --jini-chat-border-strong: #c9d0da;\n --jini-chat-border-soft: #edf0f4;\n --jini-chat-subtle: #f4f5f7;\n --jini-chat-accent: #c96442;\n --jini-chat-accent-soft: #fbeee5;\n --jini-chat-danger: #e5484d;\n --jini-chat-radius: 8px;\n --jini-chat-radius-lg: 12px;\n --bg-panel: var(--jini-chat-panel);\n --bg-subtle: var(--jini-chat-subtle);\n --border: var(--jini-chat-border);\n --border-strong: var(--jini-chat-border-strong);\n --text: var(--jini-chat-text);\n --text-strong: var(--jini-chat-text-strong);\n --text-muted: var(--jini-chat-muted);\n --danger: var(--jini-chat-danger);\n --radius: var(--jini-chat-radius);\n --radius-lg: var(--jini-chat-radius-lg);\n position: relative;\n display: grid;\n grid-template-rows: auto minmax(0, 1fr) auto;\n min-width: 0;\n min-height: 0;\n height: 100%;\n /* clip after the hidden fallback: a hidden box still scrolls programmatically (a focus() or\n scrollIntoView() inside it), which shifts the pinned composer up and leaves blank pane below. */\n overflow: hidden;\n overflow: clip;\n color: var(--jini-chat-text);\n background: var(--jini-chat-bg);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Microsoft YaHei UI\", \"Noto Sans\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n font-size: 13.5px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.jini-chat-pane button,\n.jini-runtime-popover button,\n.jini-runtime-popover select {\n font: inherit;\n}\n.jini-chat-pane__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n min-height: 74px;\n padding: 18px 22px 14px;\n border-bottom: 1px solid var(--jini-chat-border-soft);\n}\n.jini-chat-pane__heading { min-width: 0; }\n.jini-chat-pane__eyebrow {\n display: block;\n margin-bottom: 4px;\n color: var(--jini-chat-muted);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 10px;\n letter-spacing: .1em;\n text-transform: uppercase;\n}\n.jini-chat-pane__title {\n overflow: hidden;\n margin: 0;\n font-size: 21px;\n font-weight: 650;\n letter-spacing: -.025em;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane__new-thread {\n padding: 8px 12px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-chat-pane__body {\n position: relative;\n min-height: 0;\n overflow: hidden;\n overflow: clip;\n}\n.jini-chat-pane .jini-message-list {\n height: 100%;\n overflow-y: auto;\n /*\n * .jini-chat-pane__controls is an absolutely-positioned overlay (not a flex/grid sibling that\n * pushes this list up), so its real height has to be reserved here instead. This used to be a\n * flat 240px guess -- and had already gone stale once before that: it started at 170px, a live\n * render outgrew it (composer + footer + working-directory row alone measured 179.75px, clipping\n * the last message's tail under the overlay), and 240px was chosen as a margin above THAT\n * measurement, not a value anything keeps in sync. useChatPaneControlsHeight now measures the\n * overlay's real height live via ResizeObserver and publishes it as\n * --jini-chat-controls-height on the pane root, so this reservation tracks the composer's\n * actual content (an attachment tray, a wrapped multi-line draft, a taller runtime-picker\n * popover) instead of a number that is only ever correct until the next thing that grows it.\n * 240px survives as the fallback for the rare environment without ResizeObserver (SSR; a test\n * harness that never polyfills it) -- everywhere else, the calc() wins.\n */\n padding: 20px 22px calc(var(--jini-chat-controls-height, 240px) + 24px);\n scrollbar-width: thin;\n}\n.jini-chat-pane .jini-message {\n margin-bottom: 20px;\n color: var(--jini-chat-text);\n font-size: 14px;\n line-height: 1.62;\n}\n/*\n * Off-screen rows skip style, layout and paint. Owner report 2026-10-11: typing in a long chat\n * lagged. React was not the cost (no row re-renders per key); the browser was. A keystroke's DOM\n * change can invalidate style for the whole document (a host dock's :has() rule did),\n * and a transcript with ~2,000 tool cards and 3.5 MB of tool output then cost ~50 ms of style\n * recalc per key. With this, only the rows on screen are recalculated (~8 ms). The DOM stays\n * whole: find-in-page, textContent readers and screen readers still see every row.\n * 'auto' in contain-intrinsic-size keeps a row's last real height once it has rendered; the\n * auto-scroll ResizeObserver re-sticks to the bottom as estimated rows resolve.\n * Containment clips paint at the row's edge, so overflow-clip-margin leaves room for focus rings\n * (2px outline + 2px offset); @supports keeps browsers without it on the old, unclipped path.\n */\n@supports (overflow-clip-margin: 8px) {\n .jini-chat-pane .jini-message {\n content-visibility: auto;\n contain-intrinsic-size: auto 160px;\n overflow-clip-margin: 8px;\n }\n}\n/* A mid-run user bubble is nested inside an assistant turn, so the preceding tool/text block\n * has no message-row bottom margin. Give it the same separation as an ordinary next turn. */\n.jini-chat-pane .jini-message-user--mid-run {\n margin-top: 20px;\n}\n/*\n * Bubble on the content, not the row: MessageRow.tsx renders attachments as siblings of\n * .jini-message-content inside .jini-message-user, each with their own chip styling -- bubbling\n * the whole row would nest one background box inside another. This replaces a dead rule this file\n * shipped with (.jini-message-row[data-role=\"user\"] / .jini-message-row.user): MessageRow.tsx\n * has never rendered a jini-message-row class or a data-role attribute, and a user message's\n * root class is jini-message-user, not bare \"user\" -- the selector never matched anything real.\n */\n.jini-chat-pane .jini-message-user .jini-message-content {\n margin-left: auto;\n padding: 10px 13px;\n width: fit-content;\n max-width: 88%;\n /* User bubbles share this style at every nesting depth, regardless of message length. */\n background: var(--jini-chat-user-bubble, #1a1b1c);\n color: var(--jini-chat-user-bubble-ink, #fff);\n border-radius: 12px 12px 3px 12px;\n}\n.jini-chat-pane .jini-message-user .jini-message-attachments {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n justify-content: flex-end;\n margin-bottom: 4px;\n}\n.jini-chat-pane .jini-message-user .jini-message-attachment-chip {\n font: inherit;\n font-size: 11px;\n line-height: 1.4;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n padding: 3px 10px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-message-user .jini-message-attachment-chip:hover {\n color: var(--jini-chat-text);\n border-color: var(--jini-chat-border-strong);\n}\n/* A sent image this browser still holds (SentAttachment.tsx): a small square thumbnail instead of\n * the filename pill. Same click target and preview modal; cover-cropped so mixed aspect ratios line up. */\n.jini-chat-pane .jini-message-user .jini-message-attachment-chip.is-thumbnail {\n width: 64px;\n height: 64px;\n padding: 0;\n overflow: hidden;\n border-radius: 10px;\n}\n.jini-chat-pane .jini-message-user .jini-message-attachment-chip.is-thumbnail:focus-visible {\n outline: 2px solid var(--jini-chat-accent);\n outline-offset: 2px;\n}\n.jini-chat-pane .jini-message-attachment-thumb {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n.jini-chat-pane .jini-message-agent {\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .02em;\n text-transform: uppercase;\n color: var(--jini-chat-faint);\n margin-bottom: 4px;\n}\n.jini-chat-pane .jini-message-error { color: var(--danger); font-size: 13px; }\n.jini-chat-pane .jini-message-pending { color: var(--jini-chat-muted); font-size: 13px; font-style: italic; }\n/*\n * The pending-turn strip: a composer-driven turn typed while a run is already streaming, rendered\n * as the newest entry in the transcript (MessageList.tsx's pendingPrompt prop) rather than a\n * banner bolted above the composer \u2014 matching Claude Code/ChatGPT's own pending-message UX\n * (previously ChatPane.tsx's ChatPaneQueuedPrompt, which rendered between the drop-target's drag\n * announcement and the composer; that component is gone).\n *\n * Owner's own framing was \"why isn't the queue message just put in the chat\" \u2014 so\n * .jini-chat-pane__queued-text below is deliberately NOT a variant banner shape; it copies\n * .jini-message-user .jini-message-content's bubble geometry line for line (width: fit-content,\n * max-width: 88%, same padding/border-radius) so it reads as the operator's own message, not a\n * system notice. What marks it as NOT YET SENT is two properties a real sent bubble never carries:\n * a dashed border (a sent bubble has none at all) and reduced opacity \u2014 chosen over the old boxed\n * strip-plus-label treatment because those two are legible at a glance without adding a second\n * visual language next to the transcript's own. The former \"QUEUED\" chip label is gone for the same\n * reason: once the bubble itself reads as pending, a label restates it. role=\"status\" plus the\n * aria-label on the row (MessageList.tsx) still carry that meaning to assistive tech that can't see\n * the dashed border. The cancel action is a separate, smaller line below the bubble (mirroring\n * .jini-message-actions--user's own placement under a real message) rather than inline inside it,\n * so it reads as secondary to the bubble, not competing with it.\n *\n * Reuses this theme's own tokens rather than the currentColor/color-mix approach reference.css uses\n * for the same rules \u2014 that file is deliberately token-free because it ships unstyled and undocked\n * from any root that defines custom properties, but this stylesheet's .jini-chat-pane root already\n * defines concrete --jini-chat-* values for every other rule here, so matching that (not\n * reference.css's host-agnostic fallback) is the actual local convention.\n * NOTE: this comment avoids backtick quoting on purpose, same as the cancel-button comment below \u2014\n * the whole stylesheet lives inside a JS template literal, so a literal backtick here would\n * terminate the string, not just this comment.\n */\n.jini-chat-pane__queued {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 4px;\n margin: 4px 0 0;\n}\n.jini-chat-pane__queued-text {\n width: fit-content;\n max-width: 88%;\n padding: 10px 13px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-subtle);\n border: 1px dashed var(--jini-chat-border);\n border-radius: 12px 12px 3px 12px;\n opacity: .68;\n overflow-wrap: break-word;\n}\n.jini-chat-pane__queued-actions {\n display: flex;\n justify-content: flex-end;\n}\n/*\n * A plain-text cancel affordance below the pending bubble, not a primary action \u2014 without this\n * reset a host's own global 'button' styling (background, border, padding, border-radius, bold\n * text) paints it as a full pill-shaped button that visually competes with (and can overlap) the\n * transcript around it \u2014 the exact failure an operator hit live in one host. '.jini-chat-pane\n * button { font: inherit }' near the top of this file already resets font-family/line-height/etc\n * to this pane's own ambient values; font-size/font-weight are redeclared here (not just left at\n * that inherited ambient size) to size this specific control down to a caption scale, well under\n * the bubble's own 14px text.\n * NOTE: this comment avoids backtick quoting on purpose (see this file's own precedent further\n * down) \u2014 the whole stylesheet lives inside a JS template literal, so a literal backtick here\n * would terminate the string, not just this comment.\n */\n.jini-chat-pane__queued-cancel {\n padding: 2px 6px;\n color: var(--jini-chat-muted);\n background: none;\n border: 0;\n border-radius: 6px;\n font-size: 11px;\n font-weight: 400;\n cursor: pointer;\n}\n.jini-chat-pane__queued-cancel:hover {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-border);\n}\n/*\n * Per-message action row (MessageRow.tsx's 'CopyMessageButton', Icon.tsx's 'copy' glyph): quiet,\n * icon-only, always present rather than a hover-only reveal, so keyboard and touch users get the\n * same access a mouse hover would give. The assistant variant carries a thin left rail (its own\n * left border) to read as attached to the turn above it without repeating that message's own\n * chrome; the user variant skips the rail and instead mirrors its bubble's own right alignment.\n * Copy is the only live button today \u2014 'jini-message-actions--assistant' is an intentional\n * extension seam, not the finished row; see that row's own call-site comment in MessageRow.tsx.\n */\n.jini-chat-pane .jini-message-actions {\n display: flex;\n align-items: center;\n gap: 2px;\n margin-top: 6px;\n}\n.jini-chat-pane .jini-message-actions--assistant {\n padding-left: 8px;\n border-left: 2px solid var(--jini-chat-border-soft);\n}\n.jini-chat-pane .jini-message-actions--user { justify-content: flex-end; }\n.jini-chat-pane .jini-message-action-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n padding: 0;\n color: var(--jini-chat-faint);\n background: none;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-message-action-btn:hover { color: var(--jini-chat-muted); background: var(--jini-chat-subtle); }\n.jini-chat-pane .jini-message-action-btn:focus-visible { outline: 2px solid var(--jini-chat-accent); outline-offset: 1px; }\n/* Visually hidden but present in the DOM (never display:none) so the polite live region beside\n each copy button still gets announced \u2014 same clip-rect technique this package already uses for\n its other visually-hidden-but-functional controls. */\n.jini-chat-pane .jini-message-copy-status {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n}\n.jini-chat-pane__controls {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 4;\n padding: 0 16px 14px;\n background: linear-gradient(to bottom, transparent, var(--jini-chat-bg) 22px);\n}\n.jini-chat-pane__suggestions {\n display: flex;\n gap: 8px;\n margin: 0 2px 8px;\n overflow-x: auto;\n}\n.jini-chat-pane__suggestion {\n flex: 0 0 auto;\n max-width: 290px;\n overflow: hidden;\n padding: 7px 11px;\n color: var(--jini-chat-muted);\n background: color-mix(in srgb, var(--jini-chat-panel) 92%, transparent);\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n font-size: 11px;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: pointer;\n}\n.jini-chat-pane__error {\n margin: 0 2px 8px;\n padding: 8px 10px;\n color: #8c2f20;\n background: #fff1ed;\n border: 1px solid #f2c8bb;\n border-radius: 8px;\n font-size: 12px;\n}\n.jini-chat-pane__error-action {\n padding: 0;\n color: inherit;\n background: none;\n border: 0;\n font: inherit;\n font-weight: 650;\n text-decoration: underline;\n cursor: pointer;\n}\n.jini-chat-pane__error-action:hover { opacity: .82; }\n.jini-chat-pane__status {\n margin: 0 2px 8px;\n padding: 8px 10px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n font-size: 12px;\n}\n.jini-chat-pane__drop-target {\n position: relative;\n margin: -8px -8px -8px;\n padding: 6px;\n border: 2px dashed transparent;\n border-radius: 16px;\n transition:\n background-color .14s ease,\n border-color .14s ease,\n box-shadow .14s ease;\n}\n.jini-chat-pane__drop-target.is-dragging-files {\n background: color-mix(in srgb, var(--jini-chat-accent-soft) 72%, transparent);\n border-color: var(--jini-chat-accent);\n box-shadow: 0 0 0 1px color-mix(in srgb, var(--jini-chat-accent) 15%, transparent);\n}\n.jini-chat-pane__drop-target.is-dragging-files .jini-composer {\n background: color-mix(in srgb, var(--jini-chat-accent-soft) 58%, var(--jini-chat-panel));\n border-color: color-mix(in srgb, var(--jini-chat-accent) 68%, var(--jini-chat-border));\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--jini-chat-accent) 10%, transparent);\n}\n.jini-chat-pane__drop-announcement {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n clip-path: inset(50%);\n}\n.jini-chat-pane .jini-composer {\n position: relative;\n overflow: visible;\n padding: 0;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 12px;\n box-shadow: none;\n transition: border-color .16s ease, box-shadow .16s ease;\n}\n.jini-chat-pane .jini-composer:focus-within {\n background: var(--jini-chat-panel);\n border-color: color-mix(in srgb, var(--jini-chat-accent) 22%, var(--jini-chat-border-strong));\n box-shadow: 0 1px 2px rgba(26, 25, 22, .06), 0 0 0 1px rgba(201, 100, 66, .06);\n}\n/*\n * Pinned-context zone: whatever a host pins above the composer input (selected plugins, MCP\n * servers, or anything a future host adds -- see 'leadingAccessories''s own doc in 'slots.ts').\n * 'Composer.tsx' only mounts this element when 'slots.leadingAccessories' is a truthy prop value,\n * but a host that always supplies a TRAY COMPONENT rather than conditionally supplying the prop\n * itself (one that internally renders 'null' when nothing is pinned) still leaves an empty node\n * behind -- ':empty' below is what actually collapses THAT case to zero height and zero seam,\n * mirroring '.jini-attachment-tray:empty' a few rules down, which solves the identical problem for\n * the real file-attachment tray.\n *\n * Collapsed (nothing pinned): zero height, zero padding, invisible (0-width) seam -- no reserved\n * space, matching the owner's \"if nil, it doesn't animate\" requirement exactly, since there is\n * nothing here to animate.\n *\n * Populated: grows to a single fixed-height row via 'max-height' (not 'height: auto', which can't\n * transition) and gains a hairline seam using the SAME border/token '.jini-composer-footer' below\n * already uses one zone down -- reads as one more zone of the same control, not a new floating\n * box. The transition runs both directions, so removing the last pinned item gets a matching exit,\n * not an instant cut.\n *\n * 'overflow-x: auto' turns this into a single scrollable row instead of the wrap-and-grow a naive\n * flex row would do with six or more items; '> *' keeps any direct host child (a single tray\n * wrapper, a bare row of buttons, anything) from shrinking to fit, so overflow -- not internal\n * wrapping -- is what kicks in once content is wider than the zone.\n *\n * The \"more content\" cue is a real shadow/vignette, not a plain alpha fade -- an alpha fade toward\n * this zone's own background was tried first and rejected on review: the chip and the zone\n * background are already close in luminance, so fading a chip's OWN pixels toward transparent read\n * as \"something is clipped/broken\", not \"swipe for more\", at a glance with no hover. The four-layer\n * 'background' below is the standard scroll-shadow technique instead: a real 'color-mix'-tinted\n * vignette (darkens in a light theme, lightens in a dark one, since it's mixed from the theme's OWN\n * text color rather than a fixed hex) drawn at each edge -- but each 'scroll'-attached shadow layer\n * is covered by a same-edge 'local'-attached solid-to-transparent layer that scrolls WITH the\n * content, so the shadow is only ever visible on the edges that still have more to reveal: at rest\n * (scrolled to the start) the left shadow is hidden and the right one shows; scroll all the way to\n * the true end and the right shadow hides too. Verified live (not just by reading the technique):\n * scrolling this element from 0 to its max scrollLeft measurably fades the right cover's computed\n * background-position across the full track and the shadow disappears exactly at the end.\n */\n.jini-chat-pane .jini-composer-leading {\n display: flex;\n align-items: center;\n overflow-x: auto;\n overflow-y: hidden;\n max-height: 0;\n padding: 0 12px;\n border-bottom: 0 solid var(--jini-chat-border-soft);\n opacity: 0;\n scrollbar-width: thin;\n transition:\n max-height .22s cubic-bezier(.3, .1, .25, 1),\n padding .22s cubic-bezier(.3, .1, .25, 1),\n border-bottom-width .22s ease,\n opacity .16s ease;\n background-color: var(--jini-chat-subtle);\n background-repeat: no-repeat;\n background-attachment: local, local, scroll, scroll;\n background-position: 0 0, 100% 0, 0 0, 100% 0;\n background-size: 26px 100%, 26px 100%, 20px 100%, 20px 100%;\n background-image:\n linear-gradient(to right, var(--jini-chat-subtle), transparent),\n linear-gradient(to left, var(--jini-chat-subtle), transparent),\n linear-gradient(to right, color-mix(in srgb, var(--jini-chat-text) 30%, transparent), transparent),\n linear-gradient(to left, color-mix(in srgb, var(--jini-chat-text) 30%, transparent), transparent);\n}\n.jini-chat-pane .jini-composer-leading:not(:empty) {\n max-height: 64px;\n padding: 10px 12px;\n border-bottom-width: 1px;\n opacity: 1;\n}\n.jini-chat-pane .jini-composer-leading > * {\n flex: none;\n min-width: max-content;\n}\n@media (prefers-reduced-motion: reduce) {\n .jini-chat-pane .jini-composer-leading { transition: none; }\n}\n.jini-chat-pane .jini-attachment-tray {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n max-height: 88px;\n overflow-y: auto;\n padding: 8px 10px 0;\n scrollbar-width: thin;\n}\n.jini-chat-pane .jini-attachment-tray:empty { display: none; }\n/*\n * The sanctioned reuse path (see '.jini-composer-leading''s own doc above and 'AttachmentTray''s\n * module doc): a host nesting Jini's own tray/chip classes inside the pinned-context zone to match\n * the real attachment tray's look. Un-does the wrap-and-grow rule directly above -- meant for that\n * tray's OWN use one zone down, where wrapping onto a second line is fine -- so the OUTER zone's\n * single scroll axis is the one that wins here instead. The real file-attachment tray is\n * unaffected: it always renders as this zone's sibling ('Composer.tsx'), never its descendant.\n */\n.jini-chat-pane .jini-composer-leading .jini-attachment-tray {\n flex-wrap: nowrap;\n max-height: none;\n overflow: visible;\n padding: 0;\n}\n/* Half the tray's width, so a sidebar-width pane fits two chips per row and its 88px cap shows two\n rows (four chips) before scrolling; at full width each chip took a row and the third was hidden. */\n.jini-chat-pane .jini-attachment-chip {\n display: inline-flex;\n align-items: center;\n box-sizing: border-box;\n min-width: 0;\n max-width: min(calc(50% - 3px), 290px);\n height: 36px;\n padding: 3px 4px 3px 5px;\n color: var(--jini-chat-text);\n background: color-mix(in srgb, var(--jini-chat-panel) 88%, var(--jini-chat-subtle));\n border: 1px solid var(--jini-chat-border);\n border-radius: 9px;\n box-shadow: 0 1px 1px rgba(13, 12, 10, .025);\n animation: jini-chat-chip-in .16s ease both;\n}\n.jini-chat-pane .jini-composer-leading .jini-attachment-chip { max-width: min(100%, 290px); }\n@keyframes jini-chat-chip-in {\n from { opacity: 0; transform: translateY(3px) scale(.94); }\n to { opacity: 1; transform: none; }\n}\n@media (prefers-reduced-motion: reduce) {\n .jini-chat-pane .jini-attachment-chip { animation: none; }\n}\n.jini-chat-pane .jini-attachment-chip-body {\n display: flex;\n align-items: center;\n min-width: 0;\n gap: 7px;\n /* A button (opens the preview modal), reset to the chip's own look. */\n padding: 0;\n font: inherit;\n color: inherit;\n text-align: left;\n background: none;\n border: 0;\n border-radius: 7px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-attachment-chip-body:focus-visible {\n outline: 2px solid var(--jini-chat-accent);\n outline-offset: 2px;\n}\n/* A staged image this browser holds: the thumbnail takes the icon tile's place, same size. */\n.jini-chat-pane .jini-attachment-chip-thumb {\n display: block;\n flex: 0 0 auto;\n width: 26px;\n height: 26px;\n object-fit: cover;\n border-radius: 7px;\n background: var(--jini-chat-subtle);\n}\n.jini-chat-pane .jini-attachment-chip-icon {\n display: grid;\n flex: 0 0 auto;\n place-items: center;\n width: 26px;\n height: 26px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-subtle);\n border-radius: 7px;\n}\n.jini-chat-pane .jini-attachment-chip-icon.is-image {\n color: var(--jini-chat-accent);\n background: var(--jini-chat-accent-soft);\n}\n.jini-chat-pane .jini-attachment-chip-copy {\n display: flex;\n min-width: 0;\n flex-direction: column;\n line-height: 1.15;\n}\n.jini-chat-pane .jini-attachment-chip-name {\n overflow: hidden;\n font-size: 11.5px;\n font-weight: 550;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-attachment-chip-size {\n color: var(--jini-chat-muted);\n font-size: 9.5px;\n}\n.jini-chat-pane .jini-attachment-remove {\n display: grid;\n flex: 0 0 auto;\n place-items: center;\n width: 25px;\n height: 25px;\n margin-left: 4px;\n padding: 0;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 0;\n border-radius: 7px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-attachment-remove:hover,\n.jini-chat-pane .jini-attachment-remove:focus-visible {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-subtle);\n outline: none;\n}\n/* Phones: the 25px \"x\" gets an invisible 44px tap area; the chip keeps its size. The area ends at the\n chip's bottom edge and grows up into the tray's 8px top padding / row gap, because growing down\n past the last row would make the 88px tray scroll by a few pixels. Rows are 8px apart here, not\n 6: at a 42px row pitch the second row's area covered the bottom 2.5px of the first row's, leaving\n the top row a 41.5px target. 8px top padding + two 36px rows + an 8px gap is exactly the 88px cap. */\n@media (max-width: 640px), (pointer: coarse) {\n .jini-chat-pane .jini-attachment-tray { row-gap: 8px; }\n .jini-chat-pane .jini-attachment-remove { position: relative; }\n .jini-chat-pane .jini-attachment-remove::before {\n content: '';\n position: absolute;\n bottom: -5.5px;\n left: 50%;\n width: 44px;\n height: 44px;\n transform: translateX(-50%);\n }\n}\n.jini-chat-pane .jini-composer-input {\n display: block;\n box-sizing: border-box;\n width: 100%;\n min-height: 92px;\n resize: none;\n padding: 12px 14px 8px;\n color: var(--jini-chat-text);\n background: transparent;\n border: 0;\n outline: 0;\n font-size: 13.5px;\n line-height: 1.6;\n}\n/* Shared focus-zoom floor: embeds at every width, all panes on small/touch viewports. */\n.jini-chat-embed .jini-chat-pane .jini-composer-input { font-size: 16px; }\n@media (max-width: 640px), (pointer: coarse) {\n .jini-chat-pane.jini-chat-pane .jini-composer-input { font-size: 16px; }\n}\n.jini-chat-pane .jini-composer-input::placeholder { color: var(--jini-chat-faint); }\n.jini-chat-pane .jini-composer-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n min-height: 42px;\n gap: 6px;\n padding: 6px 8px 8px;\n border-top: 1px solid var(--jini-chat-border-soft);\n}\n.jini-chat-pane .jini-composer-attachment-picker { display: inline-flex; }\n/* Host controls slotted at the start of the footer row, right after the attach/discovery \"+\"\n trigger above -- see ComposerSlots.footerLeadingAccessory's own doc (slots.ts) for why this\n exists as a third footer slot alongside footerAccessories/plusMenuItems. */\n.jini-chat-pane .jini-composer-footer-leading { display: inline-flex; align-items: center; }\n/* NOT 'position: relative' \u2014 the discovery popover (below) anchors off '.jini-composer' itself\n (the nearest positioned ancestor once this wrapper opts out), not off this small trigger-button\n wrapper. Anchoring to the wrapper put the popover's bottom edge at the wrapper's own top edge \u2014\n inside the footer, just below the textarea \u2014 so any popover taller than a couple of rows grew\n upward straight over the textarea and blocked clicks into it (reproduced live: Playwright's own\n click on the textarea failed with \"intercepts pointer events\" while the popover was open). */\n.jini-chat-pane .jini-composer-discovery { display: inline-flex; }\n.jini-chat-pane .jini-composer-discovery-menu,\n.jini-chat-pane .jini-composer-slash-menu {\n position: absolute;\n z-index: 8;\n display: flex;\n flex-direction: column;\n gap: 4px;\n max-height: 280px;\n overflow-y: auto;\n padding: 8px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 10px;\n box-shadow: 0 12px 30px rgb(0 0 0 / 14%);\n}\n/* Both anchor off '.jini-composer' ('bottom: calc(100% + 6px)' = fully above the textarea AND the\n footer, not just above the trigger button) so the popover floats as a clean detached card instead\n of overlapping the input it sits next to. */\n.jini-chat-pane .jini-composer-discovery-menu {\n inset-inline-start: 8px;\n bottom: calc(100% + 6px);\n width: min(280px, calc(100vw - 32px));\n}\n.jini-chat-pane .jini-composer-slash-menu {\n inset-inline: 8px;\n bottom: calc(100% + 6px);\n}\n.jini-chat-pane .jini-composer-discovery-group { display: flex; flex-direction: column; gap: 2px; }\n.jini-chat-pane .jini-composer-discovery-group-label {\n padding: 6px 8px 2px;\n color: var(--jini-chat-faint);\n font-size: 10px;\n font-weight: 700;\n letter-spacing: .08em;\n text-transform: uppercase;\n}\n.jini-chat-pane .jini-composer-discovery-item {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 2px;\n width: 100%;\n padding: 7px 8px;\n color: inherit;\n text-align: start;\n background: transparent;\n border: 0;\n border-radius: 7px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-composer-discovery-item:hover {\n background: var(--jini-chat-panel);\n}\n.jini-chat-pane .jini-composer-discovery-item.is-active {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-accent-soft);\n outline: 1px solid var(--jini-chat-accent);\n outline-offset: -1px;\n box-shadow: inset 3px 0 0 var(--jini-chat-accent);\n}\n.jini-chat-pane .jini-composer-discovery-item small { color: var(--jini-chat-muted); }\n/* The full description used to print under every row unconditionally, which is why the palette\n grew to dozens of multi-line rows on a bare \"/\" \u2014 kept in the accessibility tree at rest (never\n 'display: none', which most screen readers drop from that tree) so 'aria-describedby' keeps\n resolving whether or not anything is hovering.\n FIXED (was: opacity-only toggle, THEN an absolutely-positioned overlay) \u2014 two owner-reported\n bugs in sequence on the same element:\n 1. 'opacity: 0' alone hides a box visually but does not remove it from layout, so an\n absolutely-positioned tooltip sized to its full natural content height (routinely 50-260px\n for a real description) still counted toward its scrolling ancestor's scrollable overflow\n region while invisible ('scrollHeight' measured 501px against a 242px 'clientHeight' for a\n 6-row list), leaving a blank scrollable gap after the last real row. Fixed by collapsing the\n box itself at rest ('max-height: 0; overflow: hidden;'), not just hiding it \u2014 carried\n forward below.\n 2. That fix still left this 'position: absolute; top: 100%' of its own row (formerly anchored\n via 'position: relative' on '.jini-composer-discovery-item' above, now removed \u2014 nothing\n anchors off it anymore): 'z-index' only controls paint order, not which box a box is\n confined to, so on hover/focus the expanded tooltip painted directly over whichever row\n happened to sit below it in the SAME scrollable list (owner report: hovering \"UI/UX Design\n (Skill)\" covered the \"/mcp\" entry beneath it \u2014 not a stacking-order problem, a containment\n one). Fixed by dropping 'position: absolute' entirely: this is now a normal in-flow child of\n the row's own 'flex-direction: column' box, still collapsed to negligible size at rest via\n 'max-height: 0; overflow: hidden;', but on hover/focus it grows the ROW's own height instead\n of floating a detached box over the next row \u2014 the popover's flex column (and every row\n below) gets pushed down to make room, so it can never occlude a sibling item. This also\n retires the panel-styled \"floating card\" look (background/border/shadow): an in-flow caption\n reads correctly with the row's own muted small-text convention above, and dropping it removes\n the last reason this needed its own stacking context. 'pointer-events: none' is kept for\n parity even though nothing sits under it to protect anymore. */\n.jini-chat-pane .jini-composer-discovery-description {\n width: 100%;\n max-height: 0;\n padding: 0;\n overflow: hidden;\n color: var(--jini-chat-muted);\n font-size: 12px;\n font-weight: 400;\n line-height: 1.4;\n white-space: normal;\n opacity: 0;\n pointer-events: none;\n transition: opacity .12s ease;\n}\n.jini-chat-pane .jini-composer-discovery-item:hover .jini-composer-discovery-description,\n.jini-chat-pane .jini-composer-discovery-item:focus-visible .jini-composer-discovery-description {\n max-height: none;\n padding-top: 2px;\n overflow: visible;\n opacity: 1;\n}\n/* '<code>'/'<small>' both default to the browser's UA styling (monospace at an unrelated size)\n with nothing here overriding it \u2014 computed font-SIZE happened to already match the row label\n (13.5px), so the visible mismatch is font-FAMILY: raw monospace next to the label's sans-serif\n stack reads as a different, larger type scale even at equal size. Inheriting the row's own font\n puts both on one consistent scale. */\n.jini-chat-pane .jini-composer-slash-argument,\n.jini-chat-pane .jini-composer-slash-confirm-badge {\n font-family: inherit;\n font-size: inherit;\n font-weight: 400;\n color: var(--jini-chat-muted);\n}\n.jini-chat-pane .jini-composer-file-input {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n clip-path: inset(50%);\n}\n.jini-chat-pane .jini-composer-attach {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n padding: 0;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 1px solid transparent;\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-composer-attach:hover:not(:disabled) {\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border-color: var(--jini-chat-border);\n}\n.jini-chat-pane .jini-composer-attach:disabled { opacity: .45; cursor: default; }\n/* The working-directory trigger button reuses '.jini-composer-attach' itself (Composer.tsx) rather\n than a class duplicated here, so it is byte-identical in size/hit-area/hover/focus to its\n neighbour rather than an approximation that could drift out of sync. Only the wrapper and its\n popover are new. NOT 'position: relative' on the wrapper, for the same reason noted above\n '.jini-composer-discovery': the popover's un-measured first paint (before the layout effect sets\n its 'position: fixed' inline style) falls back to this rule's 'position: absolute', which needs\n '.jini-composer' itself \u2014 the nearest positioned ancestor \u2014 not this wrapper, to anchor against. */\n.jini-chat-pane .jini-composer-workdir { display: inline-flex; }\n.jini-chat-pane .jini-composer-workdir-panel {\n position: absolute;\n z-index: 8;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px;\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 10px;\n box-shadow: 0 12px 30px rgb(0 0 0 / 14%);\n}\n.jini-chat-pane .jini-composer-workdir-input {\n flex: 1;\n min-width: 160px;\n padding: 6px 8px;\n color: var(--jini-chat-text);\n font: inherit;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 6px;\n}\n.jini-chat-pane .jini-composer-workdir-confirm {\n padding: 6px 10px;\n color: white;\n white-space: nowrap;\n background: var(--jini-chat-text-strong);\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n/* `.jini-icon-spin` is `<Icon name=\"spinner\">` (the tool-row \"Running\" badge). Its keyframes used to\n live only in reference.css, which no host imports, so the badge sat still. */\n.jini-composer-spinner,\n.jini-runtime-spinner,\n.jini-icon-spin { animation: jini-chat-spin .8s linear infinite; }\n@keyframes jini-chat-spin { to { transform: rotate(360deg); } }\n.jini-chat-pane .jini-composer-footer-accessories {\n display: flex;\n margin-left: auto;\n min-width: 0;\n}\n.jini-chat-pane .jini-composer-send-group {\n display: flex;\n flex-shrink: 0;\n gap: 6px;\n}\n/* A footer with no attach/accessory controls before the send group (a public visitor chat) would\n leave the lone send button at the start under the footer's space-between; keep it at the end. */\n.jini-chat-pane .jini-composer-send-group:first-child { margin-left: auto; }\n.jini-chat-pane .jini-composer-send {\n display: grid;\n place-items: center;\n width: 32px;\n height: 32px;\n padding: 0;\n color: white;\n background: var(--jini-chat-text-strong);\n border: 0;\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-composer-send:disabled {\n color: #aaa59e;\n background: #ece8e2;\n cursor: default;\n}\n.jini-chat-pane__workdir {\n display: flex;\n align-items: center;\n gap: 7px;\n min-width: 0;\n padding: 5px 4px 0;\n color: var(--jini-chat-muted);\n font-size: 13px;\n}\n.jini-chat-pane__workdir code {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-working-dir-picker {\n position: relative;\n display: inline-flex;\n align-items: flex-start;\n}\n.jini-chat-pane .jini-working-dir-trigger-row {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n}\n.jini-chat-pane .jini-working-dir-trigger {\n appearance: none;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 6px;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 0;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-working-dir-trigger:hover,\n.jini-chat-pane .jini-working-dir-trigger:focus-visible {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-subtle);\n outline: none;\n}\n.jini-chat-pane .jini-working-dir-trigger.invalid { color: var(--danger); }\n.jini-chat-pane .jini-working-dir-trigger-icon { flex: 0 0 auto; opacity: .75; }\n.jini-chat-pane .jini-working-dir-trigger.invalid .jini-working-dir-trigger-icon {\n color: var(--danger);\n opacity: 1;\n}\n.jini-chat-pane .jini-working-dir-trigger-label {\n max-width: 240px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-working-dir-trigger-chevron { flex: 0 0 auto; opacity: .55; }\n.jini-chat-pane .jini-working-dir-panel,\n.jini-chat-pane .jini-working-dir-flyout {\n position: absolute;\n z-index: 90;\n display: flex;\n flex-direction: column;\n min-width: 210px;\n padding: 5px;\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border-strong);\n border-radius: 12px;\n box-shadow: 0 1px 2px rgba(0, 0, 0, .04), 0 10px 28px -10px rgba(0, 0, 0, .18);\n}\n.jini-chat-pane .jini-working-dir-panel {\n top: calc(100% + 6px);\n left: 0;\n}\n.jini-chat-pane .jini-working-dir-panel.up {\n top: auto;\n bottom: calc(100% + 6px);\n}\n.jini-chat-pane .jini-working-dir-item {\n appearance: none;\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n min-height: 32px;\n padding: 0 9px;\n color: var(--jini-chat-text-strong);\n background: transparent;\n border: 0;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n text-align: left;\n cursor: pointer;\n}\n.jini-chat-pane .jini-working-dir-item:hover,\n.jini-chat-pane .jini-working-dir-item:focus-visible,\n.jini-chat-pane .jini-working-dir-recent-item:hover,\n.jini-chat-pane .jini-working-dir-recent-item:focus-visible {\n background: var(--jini-chat-subtle);\n outline: none;\n}\n.jini-chat-pane .jini-working-dir-item-icon { flex: 0 0 auto; opacity: .8; }\n.jini-chat-pane .jini-working-dir-item > span {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-working-dir-item-chevron { margin-left: auto; opacity: .55; }\n.jini-chat-pane .jini-working-dir-submenu-row { position: relative; }\n.jini-chat-pane .jini-working-dir-flyout {\n top: -5px;\n left: 100%;\n z-index: 91;\n min-width: 220px;\n max-width: 320px;\n margin-left: 4px;\n}\n.jini-chat-pane .jini-working-dir-flyout.up { top: auto; bottom: -5px; }\n.jini-chat-pane .jini-working-dir-flyout::before {\n position: absolute;\n top: 0;\n left: -8px;\n width: 8px;\n height: 100%;\n content: \"\";\n}\n.jini-chat-pane .jini-working-dir-recent-item {\n appearance: none;\n display: grid;\n grid-template-rows: auto auto;\n grid-template-columns: auto 1fr;\n align-items: center;\n width: 100%;\n min-height: 36px;\n padding: 4px 9px;\n color: var(--jini-chat-text-strong);\n background: transparent;\n border: 0;\n border-radius: 8px;\n font: inherit;\n text-align: left;\n cursor: pointer;\n column-gap: 9px;\n}\n.jini-chat-pane .jini-working-dir-recent-item .jini-working-dir-item-icon {\n grid-row: 1 / span 2;\n}\n.jini-chat-pane .jini-working-dir-recent-name,\n.jini-chat-pane .jini-working-dir-recent-path {\n grid-column: 2;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-working-dir-recent-name { font-size: 13px; }\n.jini-chat-pane .jini-working-dir-recent-path {\n color: var(--jini-chat-muted);\n font-size: 11px;\n opacity: .7;\n}\n.jini-chat-pane .jini-working-dir-empty {\n padding: 8px 10px;\n color: var(--jini-chat-muted);\n font-size: 12px;\n white-space: nowrap;\n opacity: .7;\n}\n.jini-runtime-picker { position: relative; }\n.jini-runtime-trigger {\n display: inline-grid;\n grid-template-columns: 24px 14px;\n align-items: center;\n gap: 7px;\n height: 32px;\n min-width: 0;\n padding: 3px 7px 3px 5px;\n color: var(--jini-chat-text);\n background: transparent;\n border: 0;\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-runtime-trigger:hover { background: var(--jini-chat-subtle); }\n.jini-runtime-trigger__copy { display: none; }\n.jini-runtime-trigger__copy strong {\n overflow: hidden;\n max-width: 122px;\n font-size: 11px;\n font-weight: 650;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-runtime-trigger__copy small {\n overflow: hidden;\n max-width: 122px;\n color: var(--jini-chat-muted);\n font-size: 9px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-runtime-trigger__chevron { color: var(--jini-chat-muted); font-size: 12px; }\n.jini-runtime-agent-icon.agent-icon { flex: 0 0 auto; object-fit: contain; }\n.jini-runtime-agent-icon.agent-icon-mono {\n display: inline-block;\n background-color: currentColor;\n -webkit-mask-repeat: no-repeat;\n mask-repeat: no-repeat;\n -webkit-mask-position: center;\n mask-position: center;\n -webkit-mask-size: contain;\n mask-size: contain;\n}\n.jini-runtime-agent-icon.agent-icon-fallback {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: #74716b;\n background: #f4f5f7;\n border: 1px solid #e1e5eb;\n border-radius: 6px;\n font-weight: 700;\n}\n.jini-runtime-popover {\n --jini-chat-panel: #fdfcfa;\n --jini-chat-text: #1a1916;\n --jini-chat-text-strong: #0d0c0a;\n --jini-chat-muted: #74716b;\n --jini-chat-faint: #b3b0a8;\n --jini-chat-border: #e1e5eb;\n --jini-chat-border-soft: #edf0f4;\n --jini-chat-subtle: #f4f5f7;\n box-sizing: border-box;\n padding: 8px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 12px;\n box-shadow: 0 18px 46px rgba(13, 12, 10, .15), 0 2px 8px rgba(13, 12, 10, .06);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Microsoft YaHei UI\", \"Noto Sans\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n font-size: 13.5px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n}\n.jini-runtime-popover__head {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 10px 12px 12px;\n border-bottom: 1px solid var(--jini-chat-border-soft);\n}\n.jini-runtime-popover__head strong { color: var(--jini-chat-text-strong); font-size: 14px; font-weight: 650; }\n.jini-runtime-popover__head span { color: var(--jini-chat-muted); font-size: 12px; }\n.jini-runtime-mode,\n.jini-runtime-agent,\n.jini-runtime-rescan {\n display: flex;\n align-items: center;\n gap: 11px;\n box-sizing: border-box;\n width: 100%;\n min-height: 40px;\n padding: 8px 11px;\n color: var(--jini-chat-text);\n background: transparent;\n border: 0;\n border-radius: 8px;\n text-align: left;\n cursor: pointer;\n}\n.jini-runtime-mode:hover,\n.jini-runtime-agent:hover:not(:disabled),\n.jini-runtime-rescan:hover:not(:disabled) { background: var(--jini-chat-subtle); }\n.jini-runtime-mode.is-active { font-weight: 650; }\n.jini-runtime-mode:disabled,\n.jini-runtime-agent:disabled,\n.jini-runtime-rescan:disabled { color: var(--jini-chat-faint); cursor: default; }\n.jini-runtime-mode__meta,\n.jini-runtime-agent__status {\n margin-left: auto;\n color: var(--jini-chat-muted);\n font-size: 12px;\n white-space: nowrap;\n}\n.jini-runtime-check { flex: 0 0 auto; color: var(--jini-chat-text); }\n/* Matches .jini-message-attachment-chip/.jini-md-table-expand's pill chrome (same\n background/border/radius formula) so this reads as the same badge language already used\n elsewhere in the pane, not a new one. Sits between the agent name and its status column \u2014\n flex: 0 0 auto keeps it from stretching or shrinking when the row is tight. */\n.jini-runtime-agent__badge {\n flex: 0 0 auto;\n padding: 2px 8px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n font-size: 10px;\n font-weight: 500;\n white-space: nowrap;\n}\n.jini-runtime-section-label {\n padding: 12px 11px 5px;\n color: var(--jini-chat-faint);\n font-size: 11px;\n font-weight: 650;\n letter-spacing: .08em;\n text-transform: uppercase;\n}\n.jini-runtime-agent.is-active { background: transparent; }\n.jini-runtime-agent__copy {\n display: flex;\n flex: 1;\n flex-direction: column;\n min-width: 0;\n}\n.jini-runtime-agent__copy strong { font-size: 13.5px; font-weight: 500; }\n.jini-runtime-agent__copy small {\n overflow: hidden;\n color: #8b857e;\n font-size: 10px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-runtime-empty { padding: 10px 11px; color: var(--jini-chat-muted); font-size: 12px; }\n.jini-runtime-models {\n display: flex;\n flex-direction: column;\n gap: 9px;\n margin-top: 5px;\n padding: 10px 11px 8px;\n border-top: 1px dashed var(--jini-chat-border);\n}\n.jini-runtime-select {\n display: flex;\n flex-direction: column;\n gap: 5px;\n color: var(--jini-chat-muted);\n font-size: 13px;\n}\n.jini-runtime-select select {\n box-sizing: border-box;\n width: 100%;\n min-height: 40px;\n padding: 7px 10px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n}\n/*\n * The BYOK model, rendered as a value rather than a control \u2014 see RuntimeByokDetails for why it\n * is not a disabled select. It occupies the same box the sibling selects do so the row still\n * reads as part of the same list, but carries no border: a bordered box with no affordance is\n * exactly the \"looks editable, isn't\" shape the plain text exists to avoid.\n */\n/*\n * The BYOK model list, which CustomSelect portals to document.body. runtimePopoverPosition puts\n * the runtime popover at z-index 1000; the shared .jini-select-menu default is 60, which is right\n * everywhere else and puts this menu behind the very popover that opened it. One above the\n * popover, not far above: it must clear its own surface and nothing else.\n */\n.jini-select-menu.jini-runtime-model-menu {\n z-index: 1001;\n}\n.jini-runtime-byok-model strong {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n min-height: 40px;\n padding: 7px 10px;\n color: var(--jini-chat-text);\n font-weight: 600;\n word-break: break-all;\n}\n/*\n * Tool-call cards (ToolCard.tsx) and the per-turn usage summary (MessageRow.tsx) ship no CSS of\n * their own \u2014 see this file's own header note on why the package supplies a default theme at all.\n * Collapsed by default (.op-card-head is the only always-visible part), so a run with a dozen tool\n * calls reads as a dozen one-line rows, not a page of raw JSON.\n */\n.jini-chat-pane .jini-message-tools {\n display: flex;\n flex-direction: column;\n gap: 6px;\n width: 100%;\n}\n.jini-chat-pane .op-card {\n border: 1px solid var(--jini-chat-border);\n border-radius: var(--radius);\n background: var(--jini-chat-subtle);\n overflow: hidden;\n font-size: 13px;\n}\n.jini-chat-pane .op-card-head {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 7px 10px;\n background: none;\n border: 0;\n font: inherit;\n text-align: left;\n cursor: pointer;\n color: inherit;\n}\n.jini-chat-pane .op-status { display: inline-flex; flex-shrink: 0; }\n.jini-chat-pane .op-status-ok { color: #4e875f; }\n.jini-chat-pane .op-status-error { color: var(--danger); }\n.jini-chat-pane .op-status-running { color: var(--jini-chat-accent); }\n.jini-chat-pane .op-title { font-weight: 600; flex-shrink: 0; }\n.jini-chat-pane .shimmer-text { animation: jini-chat-shimmer 1.6s ease-in-out infinite; }\n@keyframes jini-chat-shimmer { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }\n@media (prefers-reduced-motion: reduce) { .jini-chat-pane .shimmer-text { animation: none; opacity: .6; } }\n.jini-chat-pane .op-meta {\n color: var(--jini-chat-muted);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: 1;\n min-width: 0;\n}\n.jini-chat-pane .op-expand-chev { flex-shrink: 0; color: var(--jini-chat-faint); }\n/* Collapsed by default: 0 max-height clips the inner content, no display:none \u2014 keeps the height\n transition instead of an instant jump cut. */\n.jini-chat-pane .accordion-collapsible { max-height: 0; overflow: hidden; transition: max-height .15s ease; }\n.jini-chat-pane .accordion-collapsible.open { max-height: 480px; overflow-y: auto; }\n.jini-chat-pane .accordion-collapsible-inner { padding: 0 10px 10px; }\n.jini-chat-pane .op-card-detail { display: flex; flex-direction: column; gap: 6px; }\n.jini-chat-pane .op-path,\n.jini-chat-pane .op-command,\n.jini-chat-pane .op-output {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12px;\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 6px;\n padding: 6px 8px;\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n}\n/* The \"Waiting for the result or answer\u2026\" / \"Finished with no output.\" line in an expanded tool row (ToolCard.tsx's ToolCallDetail). */\n.jini-chat-pane .op-detail-note { margin: 0; font-size: 12px; color: var(--jini-chat-muted); }\n.jini-chat-pane .op-open {\n border: 1px solid var(--jini-chat-border-strong);\n border-radius: 6px;\n background: var(--jini-chat-panel);\n padding: 2px 8px;\n font-size: 11px;\n cursor: pointer;\n}\n/* The \"Done \u00B7 6m 29s \u00B7 2612 out \u00B7 $0.4028\" line \u2014 real numbers from the run's own kind:'usage' event. */\n.jini-chat-pane .jini-message-usage {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n color: var(--jini-chat-muted);\n margin-top: 2px;\n}\n.jini-chat-pane .jini-message-usage-dot { font-size: 8px; color: #4e875f; }\n/* toolCallDisplay 'collapsed' (ToolCallGroup.tsx): one quiet line per turn, cards open under it. */\n.jini-chat-pane .jini-tool-group { display: flex; flex-direction: column; gap: 6px; width: 100%; }\n.jini-chat-pane .jini-tool-group__toggle {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n align-self: flex-start;\n max-width: 100%;\n padding: 2px 0;\n background: none;\n border: 0;\n font: inherit;\n font-size: 12px;\n color: var(--jini-chat-muted);\n cursor: pointer;\n}\n.jini-chat-pane .jini-tool-group__toggle:hover { color: inherit; }\n.jini-chat-pane .jini-tool-group__toggle:focus-visible { outline: 2px solid var(--jini-chat-accent); outline-offset: 2px; border-radius: 4px; }\n.jini-chat-pane .jini-tool-group__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.jini-chat-pane .jini-tool-group[data-failed=\"true\"] .jini-tool-group__toggle { color: var(--danger); }\n/* showThinking (MessageRow.tsx's ThinkingBlock): the model's reasoning, folded, set apart from the answer. */\n.jini-chat-pane .jini-message-thinking { font-size: 12px; color: var(--jini-chat-muted); }\n.jini-chat-pane .jini-message-thinking > summary { cursor: pointer; width: fit-content; }\n.jini-chat-pane .jini-message-thinking__text {\n margin-top: 4px;\n padding-left: 10px;\n border-left: 2px solid var(--jini-chat-border);\n white-space: pre-wrap;\n word-break: break-word;\n}\n/*\n * Fenced code block ('Markdown.tsx''s 'renderBlock' case 'code') and inline 'code' span visual\n * treatment \u2014 previously entirely unstyled (bare UA-default '<pre><code>', no background, border,\n * or padding; a downstream admin host's own stylesheet already flagged this gap in a note next to\n * its unrelated horizontal-scroll fix for the same element). Lives HERE, in the package's own\n * injected default theme, rather than in a host's stylesheet: a host CAN already remap the exact\n * '--jini-chat-*' custom properties this rule reads (one such host's '.admin-chat-dock .jini-chat-\n * pane' block already does, for every other rule in this file), so putting the fix here makes every\n * Jini host look correct by default AND lets a host match its own palette for free, with no new\n * host-side override rule needed at all \u2014 the reverse (host-only CSS) would leave every other\n * embedder of '@jini-ai/chat' with the same unstyled '<pre>' this fix exists to correct.\n * 'max-width'/'overflow-x' here duplicate (harmlessly) a narrower fix that same host's own\n * '.jini-message-content pre' rule already carries for its own fixed-width dock specifically \u2014 this\n * package cannot assume every host has that rule, so a real host-agnostic default belongs here too.\n * NOTE: this comment avoids backtick quoting on purpose \u2014 this whole stylesheet lives inside a JS\n * template literal, so a literal backtick here would terminate the string, not just this comment.\n */\n.jini-chat-pane .jini-message-content pre {\n margin: 6px 0;\n max-width: 100%;\n overflow-x: auto;\n padding: 10px 12px;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12.5px;\n line-height: 1.5;\n}\n.jini-chat-pane .jini-message-content pre code {\n padding: 0;\n background: none;\n border: 0;\n color: inherit;\n font: inherit;\n}\n.jini-chat-pane .jini-message-content :not(pre) > code {\n padding: .15em .4em;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 4px;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: .9em;\n}\n/*\n * Copy affordance for a fenced code block ('Markdown.tsx''s 'CodeBlock'/'CopyCodeButton'). The\n * button lives in its own toolbar row fused to the TOP of the '<pre>' below, not overlaid on top\n * of the code itself \u2014 an overlay would sit over whatever text happens to render at that corner,\n * and '<pre>' already carries its own horizontal scrollbar along its own bottom edge, so a row\n * above it can never collide with that scrollbar either. '.jini-md-code-block' takes over the\n * '<pre>' rule's own top/bottom margin above so the two don't stack, and the toolbar/pre pair\n * share one rounded box (toolbar keeps the top corners, '<pre>' below loses them) instead of two\n * separately-bordered boxes stacked on each other.\n */\n.jini-chat-pane .jini-message-content .jini-md-code-block {\n margin: 6px 0;\n}\n.jini-chat-pane .jini-message-content .jini-md-code-block pre {\n margin: 0;\n border-top: 0;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.jini-chat-pane .jini-md-code-toolbar {\n display: flex;\n justify-content: flex-end;\n padding: 3px;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-bottom: 0;\n border-radius: 8px 8px 0 0;\n}\n.jini-chat-pane .jini-md-code-copy {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n padding: 0;\n color: var(--jini-chat-faint);\n background: none;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-md-code-copy:hover { color: var(--jini-chat-muted); background: var(--jini-chat-border); }\n.jini-chat-pane .jini-md-code-copy:focus-visible { outline: 2px solid var(--jini-chat-accent); outline-offset: 1px; }\n/* Visually hidden but present in the DOM (never display:none) so this polite live region still\n gets announced \u2014 same clip-rect technique 'jini-message-copy-status' above already uses. */\n.jini-chat-pane .jini-md-code-copy-status {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n}\n/*\n * GFM pipe tables ('Markdown.tsx''s 'TableBlock') \u2014 same reasoning as the code-block rules directly\n * above for living in the package's own default theme rather than a host stylesheet. '.jini-md-\n * table-wrap' gets its own horizontal scrollbar so a wide table scrolls internally instead of\n * dragging '.jini-message-list' sideways with it (the exact failure mode that same downstream host's\n * stylesheet documents for the code-block case \u2014 the same fix shape applies here). The \"Expand\n * table\" button only renders ('TableBlock''s own logic) once the table has genuinely overflowed\n * that wrap.\n */\n.jini-md-table-wrap {\n max-width: 100%;\n overflow-x: auto;\n}\n.jini-md-table {\n border-collapse: collapse;\n font-size: 12.5px;\n}\n/*\n * 'min-width' here, not on '.jini-md-table' itself, and picked per column rather than as one flat\n * table-wide number -- a flat table minimum sized for a typical 2-column table would leave a\n * 5-column table crushed just as badly as before (same failure this comment's neighbor above\n * describes), while one sized for 5 columns would force pointless overflow on a simple 2-column\n * table. Per-cell 'min-width' scales the effective table floor with the real column count instead.\n * 130px was picked empirically, not guessed: measured (real Chromium, not jsdom -- 'scrollWidth'/\n * 'clientWidth' are unimplemented there) against this package's narrowest known real host, an\n * admin chat dock fixed at 380px wide ('admin-chat-dock' in that host's own stylesheet), which\n * nets out to roughly 238px of usable width once that dock's own message-list padding, the 80%\n * message-bubble cap, and this content's own padding are all applied. Below 120px per column the\n * table still fits inside those 238px and never overflows -- the exact bug this fix exists to\n * correct. 130px was chosen just above that measured 120px crossover (a small margin so it clears\n * reliably rather than sitting exactly on the boundary) and reads as a genuinely defensible per-\n * column floor on its own terms too: with this rule's own 6px/10px cell padding subtracted, it\n * leaves about 110px of text room, roughly 14-16 characters per wrapped line at this rule's 12.5px\n * font -- enough that a table cell wraps in short phrases instead of one word per line, which is\n * the readability floor this fix is actually chasing. One real consequence, verified rather than\n * hand-waved: at this width, ANY 2-column table in that narrow dock now overflows and gets the\n * \"Expand table\" affordance, including ones with short cell content -- there is no per-cell\n * min-width value that can overflow a long-prose 2-column table without also overflowing a short\n * one at the same column count, because the browser's own auto-layout table-width algorithm sizes\n * the table to the greater of its container's width and the sum of each column's own minimum --\n * content length past that minimum only changes how much a column wraps internally, never whether\n * the table exceeds its wrap. Accepted here: in a surface already this narrow, showing \"Expand\n * table\" a little more eagerly is the better failure mode than silently crushing a table's own\n * content illegibly, which is the state this whole rule exists to fix.\n */\n.jini-md-table th,\n.jini-md-table td {\n padding: 6px 10px;\n min-width: 130px;\n border: 1px solid var(--jini-chat-border);\n text-align: left;\n}\n.jini-md-table th {\n background: var(--jini-chat-subtle);\n color: var(--jini-chat-text-strong);\n font-weight: 650;\n}\n.jini-md-table-expand {\n margin-top: 4px;\n padding: 4px 10px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n font-size: 11px;\n cursor: pointer;\n}\n.jini-md-table-expand:hover { color: var(--jini-chat-text-strong); }\n.jini-md-table-modal {\n max-width: min(90vw, 900px);\n max-height: 80vh;\n overflow: auto;\n padding: 20px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 12px;\n box-shadow: 0 18px 46px rgba(13, 12, 10, .15), 0 2px 8px rgba(13, 12, 10, .06);\n}\n.jini-md-table-modal::backdrop {\n background: rgba(0, 0, 0, .4);\n}\n.jini-md-table-modal-close {\n position: absolute;\n top: 10px;\n right: 10px;\n width: 28px;\n height: 28px;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 0;\n border-radius: 6px;\n font-size: 16px;\n line-height: 1;\n cursor: pointer;\n}\n.jini-md-table-modal-close:hover {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-subtle);\n}\n@media (max-width: 560px) {\n .jini-chat-pane__header { padding-inline: 16px; }\n .jini-chat-pane .jini-message-list { padding-inline: 16px; }\n .jini-chat-pane__controls { padding-inline: 10px; }\n}\n/*\n * Attachment preview modal (MessageRow.tsx's chip -> AttachmentPreviewModal.tsx). Same\n * native-<dialog> shape as .jini-md-table-modal above (own box sized to content, not viewport;\n * ::backdrop dims the page), kept as its own class rather than reusing that one because the two\n * differ in intent: a table pop-out is a fixed-chrome expansion of content already on screen, this\n * is a general image/text/metadata viewer with its own header and a dedicated close control.\n */\n.jini-attachment-preview-dialog {\n display: flex;\n flex-direction: column;\n width: min(90vw, 720px);\n max-height: 80vh;\n padding: 0;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 12px;\n box-shadow: 0 18px 46px rgba(13, 12, 10, .15), 0 2px 8px rgba(13, 12, 10, .06);\n}\n.jini-attachment-preview-dialog::backdrop {\n background: rgba(0, 0, 0, .4);\n}\n.jini-attachment-preview-header {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--jini-chat-border-soft);\n}\n.jini-attachment-preview-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n font-weight: 650;\n color: var(--jini-chat-text-strong);\n}\n.jini-attachment-preview-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 auto;\n width: 26px;\n height: 26px;\n margin-left: auto;\n padding: 0;\n color: var(--jini-chat-muted);\n background: none;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n.jini-attachment-preview-close:hover {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-subtle);\n}\n.jini-attachment-preview-body {\n display: flex;\n flex: 1;\n min-height: 0;\n align-items: center;\n justify-content: center;\n padding: 16px;\n overflow: auto;\n}\n.jini-attachment-preview-image {\n display: block;\n max-width: 100%;\n max-height: calc(80vh - 90px);\n object-fit: contain;\n margin: auto;\n}\n.jini-attachment-preview-text {\n width: 100%;\n max-height: calc(80vh - 90px);\n margin: 0;\n padding: 12px;\n overflow: auto;\n color: var(--jini-chat-text);\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border-soft);\n border-radius: 8px;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre-wrap;\n word-break: break-word;\n}\n.jini-attachment-preview-truncated {\n width: 100%;\n margin-top: 8px;\n color: var(--jini-chat-muted);\n font-size: 11px;\n text-align: center;\n}\n.jini-attachment-preview-loading {\n color: var(--jini-chat-muted);\n font-size: 13px;\n font-style: italic;\n}\n.jini-attachment-preview-meta {\n display: flex;\n flex-direction: column;\n gap: 6px;\n width: 100%;\n max-width: 360px;\n color: var(--jini-chat-text);\n font-size: 13px;\n}\n.jini-attachment-preview-meta-row {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n}\n.jini-attachment-preview-meta-label {\n color: var(--jini-chat-faint);\n}\n.jini-attachment-preview-meta-note {\n margin-top: 6px;\n color: var(--jini-chat-muted);\n font-size: 12px;\n}\n/* Previous/next across a message's (or the composer's) images: a footer under the image, so the\n * buttons sit on the panel colour - readable over any picture - side by side in the bottom-right. */\n/* A fixed stage while stepping: sized to each image, the dialog would resize and move the buttons\n * out from under the pointer between clicks. */\n.jini-attachment-preview-dialog.has-nav .jini-attachment-preview-body {\n flex: 0 0 auto;\n height: min(calc(80vh - 152px), 480px);\n}\n.jini-attachment-preview-dialog.has-nav .jini-attachment-preview-image,\n.jini-attachment-preview-dialog.has-nav .jini-attachment-preview-text {\n max-height: 100%;\n}\n.jini-attachment-preview-nav {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 10px 14px;\n border-top: 1px solid var(--jini-chat-border-soft);\n}\n.jini-attachment-preview-count {\n margin-right: auto;\n color: var(--jini-chat-muted);\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n}\n.jini-attachment-preview-step {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n padding: 0;\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-attachment-preview-step:hover {\n background: var(--jini-chat-subtle);\n border-color: var(--jini-chat-border-strong);\n}\n.jini-attachment-preview-step:focus-visible {\n outline: 2px solid var(--jini-chat-accent);\n outline-offset: 2px;\n}\n/* Phones: 44px targets. The 26px close \"x\" keeps its size inside an invisible 44px area (centred,\n so it stays within the header's 12px/14px padding); the bordered previous/next buttons just grow. */\n@media (max-width: 640px), (pointer: coarse) {\n .jini-attachment-preview-close { position: relative; }\n .jini-attachment-preview-close::before {\n content: '';\n position: absolute;\n top: 50%;\n left: 50%;\n width: 44px;\n height: 44px;\n transform: translate(-50%, -50%);\n }\n .jini-attachment-preview-step {\n width: 44px;\n height: 44px;\n }\n}\n";
1
+ export declare const CHAT_PANE_STYLES = "\n.jini-chat-pane {\n --jini-chat-bg: #faf9f7;\n --jini-chat-panel: #fdfcfa;\n --jini-chat-text: #1a1916;\n --jini-chat-text-strong: #0d0c0a;\n --jini-chat-muted: #74716b;\n --jini-chat-faint: #b3b0a8;\n --jini-chat-border: #e1e5eb;\n --jini-chat-border-strong: #c9d0da;\n --jini-chat-border-soft: #edf0f4;\n --jini-chat-subtle: #f4f5f7;\n --jini-chat-accent: #c96442;\n --jini-chat-accent-soft: #fbeee5;\n --jini-chat-danger: #e5484d;\n --jini-chat-radius: 8px;\n --jini-chat-radius-lg: 12px;\n --bg-panel: var(--jini-chat-panel);\n --bg-subtle: var(--jini-chat-subtle);\n --border: var(--jini-chat-border);\n --border-strong: var(--jini-chat-border-strong);\n --text: var(--jini-chat-text);\n --text-strong: var(--jini-chat-text-strong);\n --text-muted: var(--jini-chat-muted);\n --danger: var(--jini-chat-danger);\n --radius: var(--jini-chat-radius);\n --radius-lg: var(--jini-chat-radius-lg);\n position: relative;\n display: grid;\n grid-template-rows: auto minmax(0, 1fr) auto;\n min-width: 0;\n min-height: 0;\n height: 100%;\n /* clip after the hidden fallback: a hidden box still scrolls programmatically (a focus() or\n scrollIntoView() inside it), which shifts the pinned composer up and leaves blank pane below. */\n overflow: hidden;\n overflow: clip;\n color: var(--jini-chat-text);\n background: var(--jini-chat-bg);\n font-family: var(--jini-chat-font-body, -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Microsoft YaHei UI\", \"Noto Sans\", Roboto, \"Helvetica Neue\", Arial, sans-serif);\n font-size: 13.5px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.jini-chat-pane button,\n.jini-runtime-popover button,\n.jini-runtime-popover select {\n font: inherit;\n}\n.jini-chat-pane__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n min-height: 74px;\n padding: 18px 22px 14px;\n border-bottom: 1px solid var(--jini-chat-border-soft);\n}\n.jini-chat-pane__heading { min-width: 0; }\n.jini-chat-pane__eyebrow {\n display: block;\n margin-bottom: 4px;\n color: var(--jini-chat-muted);\n font-family: var(--jini-chat-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);\n font-size: 10px;\n letter-spacing: .1em;\n text-transform: uppercase;\n}\n.jini-chat-pane__title {\n overflow: hidden;\n margin: 0;\n font-size: 21px;\n font-weight: 650;\n letter-spacing: -.025em;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane__new-thread {\n padding: 8px 12px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-chat-pane__body {\n position: relative;\n min-height: 0;\n overflow: hidden;\n overflow: clip;\n}\n.jini-chat-pane .jini-message-list {\n height: 100%;\n overflow-y: auto;\n /*\n * .jini-chat-pane__controls is an absolutely-positioned overlay (not a flex/grid sibling that\n * pushes this list up), so its real height has to be reserved here instead. This used to be a\n * flat 240px guess -- and had already gone stale once before that: it started at 170px, a live\n * render outgrew it (composer + footer + working-directory row alone measured 179.75px, clipping\n * the last message's tail under the overlay), and 240px was chosen as a margin above THAT\n * measurement, not a value anything keeps in sync. useChatPaneControlsHeight now measures the\n * overlay's real height live via ResizeObserver and publishes it as\n * --jini-chat-controls-height on the pane root, so this reservation tracks the composer's\n * actual content (an attachment tray, a wrapped multi-line draft, a taller runtime-picker\n * popover) instead of a number that is only ever correct until the next thing that grows it.\n * 240px survives as the fallback for the rare environment without ResizeObserver (SSR; a test\n * harness that never polyfills it) -- everywhere else, the calc() wins.\n */\n padding: 20px 22px calc(var(--jini-chat-controls-height, 240px) + 24px);\n scrollbar-width: thin;\n}\n/*\n * 'Jump to latest' (MessageList.tsx's JumpToLatest): a zero-height sticky last child, so it floats\n * centred just above the composer overlay without taking layout space. Sticky offsets measure from\n * the scrollport edge, not the padding, hence the controls height in 'bottom'.\n */\n.jini-message-list__jump {\n position: sticky;\n bottom: calc(var(--jini-chat-controls-height, 240px) + 8px);\n height: 0;\n display: flex;\n justify-content: center;\n align-items: flex-end;\n z-index: 2;\n pointer-events: none;\n}\n.jini-message-list__jump-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 5px 12px;\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n background: var(--jini-chat-panel);\n color: var(--jini-chat-muted);\n box-shadow: var(--jini-chat-popover-shadow);\n font-size: 12px;\n line-height: 1.2;\n white-space: nowrap;\n cursor: pointer;\n pointer-events: auto;\n}\n.jini-message-list__jump-btn:hover { color: var(--jini-chat-text-strong); }\n.jini-message-list__jump-btn:focus-visible { outline: 2px solid var(--jini-chat-accent); outline-offset: 2px; }\n.jini-chat-pane .jini-message {\n margin-bottom: 20px;\n color: var(--jini-chat-text);\n font-size: 14px;\n line-height: 1.62;\n}\n/*\n * Off-screen rows skip style, layout and paint. Owner report 2026-10-11: typing in a long chat\n * lagged. React was not the cost (no row re-renders per key); the browser was. A keystroke's DOM\n * change can invalidate style for the whole document (a host dock's :has() rule did),\n * and a transcript with ~2,000 tool cards and 3.5 MB of tool output then cost ~50 ms of style\n * recalc per key. With this, only the rows on screen are recalculated (~8 ms). The DOM stays\n * whole: find-in-page, textContent readers and screen readers still see every row.\n * 'auto' in contain-intrinsic-size keeps a row's last real height once it has rendered; the\n * auto-scroll ResizeObserver re-sticks to the bottom as estimated rows resolve.\n * Containment clips paint at the row's edge, so overflow-clip-margin leaves room for focus rings\n * (2px outline + 2px offset); @supports keeps browsers without it on the old, unclipped path.\n */\n@supports (overflow-clip-margin: 8px) {\n .jini-chat-pane .jini-message {\n content-visibility: auto;\n contain-intrinsic-size: auto 160px;\n overflow-clip-margin: 8px;\n }\n}\n/* A mid-run user bubble is nested inside an assistant turn, so the preceding tool/text block\n * has no message-row bottom margin. Give it the same separation as an ordinary next turn. */\n.jini-chat-pane .jini-message-user--mid-run {\n margin-top: 20px;\n}\n/*\n * Bubble on the content, not the row: MessageRow.tsx renders attachments as siblings of\n * .jini-message-content inside .jini-message-user, each with their own chip styling -- bubbling\n * the whole row would nest one background box inside another. This replaces a dead rule this file\n * shipped with (.jini-message-row[data-role=\"user\"] / .jini-message-row.user): MessageRow.tsx\n * has never rendered a jini-message-row class or a data-role attribute, and a user message's\n * root class is jini-message-user, not bare \"user\" -- the selector never matched anything real.\n */\n.jini-chat-pane .jini-message-user .jini-message-content {\n margin-left: auto;\n padding: 10px 13px;\n width: fit-content;\n max-width: 88%;\n /* MOVE: Answers may contain long labels or pasted URLs; wrap them within narrow chat panes. */\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n /* User bubbles share this style at every nesting depth, regardless of message length. */\n background: var(--jini-chat-user-bubble, #1a1b1c);\n color: var(--jini-chat-user-bubble-ink, #fff);\n border-radius: 12px 12px 3px 12px;\n}\n.jini-chat-pane .jini-message-user .jini-message-attachments {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n justify-content: flex-end;\n margin-bottom: 4px;\n}\n.jini-chat-pane .jini-message-user .jini-message-attachment-chip {\n font: inherit;\n font-size: 11px;\n line-height: 1.4;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n padding: 3px 10px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-message-user .jini-message-attachment-chip:hover {\n color: var(--jini-chat-text);\n border-color: var(--jini-chat-border-strong);\n}\n/* A sent image this browser still holds (SentAttachment.tsx): a small square thumbnail instead of\n * the filename pill. Same click target and preview modal; cover-cropped so mixed aspect ratios line up. */\n.jini-chat-pane .jini-message-user .jini-message-attachment-chip.is-thumbnail {\n width: 64px;\n height: 64px;\n padding: 0;\n overflow: hidden;\n border-radius: 10px;\n}\n.jini-chat-pane .jini-message-user .jini-message-attachment-chip.is-thumbnail:focus-visible {\n outline: 2px solid var(--jini-chat-accent);\n outline-offset: 2px;\n}\n.jini-chat-pane .jini-message-attachment-thumb {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n.jini-chat-pane .jini-message-agent {\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .02em;\n text-transform: uppercase;\n color: var(--jini-chat-faint);\n margin-bottom: 4px;\n}\n.jini-chat-pane .jini-message-error { color: var(--danger); font-size: 13px; }\n.jini-chat-pane .jini-message-pending { color: var(--jini-chat-muted); font-size: 13px; font-style: italic; }\n/*\n * The pending-turn strip: a composer-driven turn typed while a run is already streaming, rendered\n * as the newest entry in the transcript (MessageList.tsx's pendingPrompt prop) rather than a\n * banner bolted above the composer \u2014 matching Claude Code/ChatGPT's own pending-message UX\n * (previously ChatPane.tsx's ChatPaneQueuedPrompt, which rendered between the drop-target's drag\n * announcement and the composer; that component is gone).\n *\n * Owner's own framing was \"why isn't the queue message just put in the chat\" \u2014 so\n * .jini-chat-pane__queued-text below is deliberately NOT a variant banner shape; it copies\n * .jini-message-user .jini-message-content's bubble geometry line for line (width: fit-content,\n * max-width: 88%, same padding/border-radius) so it reads as the operator's own message, not a\n * system notice. What marks it as NOT YET SENT is two properties a real sent bubble never carries:\n * a dashed border (a sent bubble has none at all) and reduced opacity \u2014 chosen over the old boxed\n * strip-plus-label treatment because those two are legible at a glance without adding a second\n * visual language next to the transcript's own. The former \"QUEUED\" chip label is gone for the same\n * reason: once the bubble itself reads as pending, a label restates it. role=\"status\" plus the\n * aria-label on the row (MessageList.tsx) still carry that meaning to assistive tech that can't see\n * the dashed border. The cancel action is a separate, smaller line below the bubble (mirroring\n * .jini-message-actions--user's own placement under a real message) rather than inline inside it,\n * so it reads as secondary to the bubble, not competing with it.\n *\n * Reuses this theme's own tokens rather than the currentColor/color-mix approach reference.css uses\n * for the same rules \u2014 that file is deliberately token-free because it ships unstyled and undocked\n * from any root that defines custom properties, but this stylesheet's .jini-chat-pane root already\n * defines concrete --jini-chat-* values for every other rule here, so matching that (not\n * reference.css's host-agnostic fallback) is the actual local convention.\n * NOTE: this comment avoids backtick quoting on purpose, same as the cancel-button comment below \u2014\n * the whole stylesheet lives inside a JS template literal, so a literal backtick here would\n * terminate the string, not just this comment.\n */\n.jini-chat-pane__queued {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 4px;\n margin: 4px 0 0;\n}\n.jini-chat-pane__queued-text {\n width: fit-content;\n max-width: 88%;\n padding: 10px 13px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-subtle);\n border: 1px dashed var(--jini-chat-border);\n border-radius: 12px 12px 3px 12px;\n opacity: .68;\n overflow-wrap: break-word;\n}\n.jini-chat-pane__queued-actions {\n display: flex;\n justify-content: flex-end;\n}\n/*\n * A plain-text cancel affordance below the pending bubble, not a primary action \u2014 without this\n * reset a host's own global 'button' styling (background, border, padding, border-radius, bold\n * text) paints it as a full pill-shaped button that visually competes with (and can overlap) the\n * transcript around it \u2014 the exact failure an operator hit live in one host. '.jini-chat-pane\n * button { font: inherit }' near the top of this file already resets font-family/line-height/etc\n * to this pane's own ambient values; font-size/font-weight are redeclared here (not just left at\n * that inherited ambient size) to size this specific control down to a caption scale, well under\n * the bubble's own 14px text.\n * NOTE: this comment avoids backtick quoting on purpose (see this file's own precedent further\n * down) \u2014 the whole stylesheet lives inside a JS template literal, so a literal backtick here\n * would terminate the string, not just this comment.\n */\n.jini-chat-pane__queued-cancel {\n padding: 2px 6px;\n color: var(--jini-chat-muted);\n background: none;\n border: 0;\n border-radius: 6px;\n font-size: 11px;\n font-weight: 400;\n cursor: pointer;\n}\n.jini-chat-pane__queued-cancel:hover {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-border);\n}\n/*\n * Per-message action row (MessageRow.tsx's 'CopyMessageButton', Icon.tsx's 'copy' glyph): quiet,\n * icon-only, always present rather than a hover-only reveal, so keyboard and touch users get the\n * same access a mouse hover would give. The assistant variant carries a thin left rail (its own\n * left border) to read as attached to the turn above it without repeating that message's own\n * chrome; the user variant skips the rail and instead mirrors its bubble's own right alignment.\n * Copy is the only live button today \u2014 'jini-message-actions--assistant' is an intentional\n * extension seam, not the finished row; see that row's own call-site comment in MessageRow.tsx.\n */\n.jini-chat-pane .jini-message-actions {\n display: flex;\n align-items: center;\n gap: 2px;\n margin-top: 6px;\n}\n.jini-chat-pane .jini-message-actions--assistant {\n padding-left: 8px;\n border-left: 2px solid var(--jini-chat-border-soft);\n}\n.jini-chat-pane .jini-message-actions--user { justify-content: flex-end; }\n.jini-chat-pane .jini-message-action-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n padding: 0;\n color: var(--jini-chat-faint);\n background: none;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-message-action-btn:hover { color: var(--jini-chat-muted); background: var(--jini-chat-subtle); }\n.jini-chat-pane .jini-message-action-btn:focus-visible { outline: 2px solid var(--jini-chat-accent); outline-offset: 1px; }\n/* Visually hidden but present in the DOM (never display:none) so the polite live region beside\n each copy button still gets announced \u2014 same clip-rect technique this package already uses for\n its other visually-hidden-but-functional controls. */\n.jini-chat-pane .jini-message-copy-status {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n}\n.jini-chat-pane__controls {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 4;\n padding: 0 16px 14px;\n background: linear-gradient(to bottom, transparent, var(--jini-chat-bg) 22px);\n}\n.jini-chat-pane__suggestions {\n display: flex;\n gap: 8px;\n margin: 0 2px 8px;\n overflow-x: auto;\n}\n.jini-chat-pane__suggestion {\n flex: 0 0 auto;\n max-width: 290px;\n overflow: hidden;\n padding: 7px 11px;\n color: var(--jini-chat-muted);\n background: color-mix(in srgb, var(--jini-chat-panel) 92%, transparent);\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n font-size: 11px;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: pointer;\n}\n.jini-chat-pane__error {\n margin: 0 2px 8px;\n padding: 8px 10px;\n color: #8c2f20;\n background: #fff1ed;\n border: 1px solid #f2c8bb;\n border-radius: 8px;\n font-size: 12px;\n}\n.jini-chat-pane__error-action {\n padding: 0;\n color: inherit;\n background: none;\n border: 0;\n font: inherit;\n font-weight: 650;\n text-decoration: underline;\n cursor: pointer;\n}\n.jini-chat-pane__error-action:hover { opacity: .82; }\n.jini-chat-pane__status {\n margin: 0 2px 8px;\n padding: 8px 10px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n font-size: 12px;\n}\n.jini-chat-pane__drop-target {\n position: relative;\n margin: -8px -8px -8px;\n padding: 6px;\n border: 2px dashed transparent;\n border-radius: 16px;\n transition:\n background-color .14s ease,\n border-color .14s ease,\n box-shadow .14s ease;\n}\n.jini-chat-pane__drop-target.is-dragging-files {\n background: color-mix(in srgb, var(--jini-chat-accent-soft) 72%, transparent);\n border-color: var(--jini-chat-accent);\n box-shadow: 0 0 0 1px color-mix(in srgb, var(--jini-chat-accent) 15%, transparent);\n}\n.jini-chat-pane__drop-target.is-dragging-files .jini-composer {\n background: color-mix(in srgb, var(--jini-chat-accent-soft) 58%, var(--jini-chat-panel));\n border-color: color-mix(in srgb, var(--jini-chat-accent) 68%, var(--jini-chat-border));\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--jini-chat-accent) 10%, transparent);\n}\n.jini-chat-pane__drop-announcement {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n clip-path: inset(50%);\n}\n.jini-chat-pane .jini-composer {\n position: relative;\n overflow: visible;\n padding: 0;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 12px;\n box-shadow: none;\n transition: border-color .16s ease, box-shadow .16s ease;\n}\n.jini-chat-pane .jini-composer:focus-within {\n background: var(--jini-chat-panel);\n border-color: color-mix(in srgb, var(--jini-chat-accent) 22%, var(--jini-chat-border-strong));\n box-shadow: 0 1px 2px rgba(26, 25, 22, .06), 0 0 0 1px rgba(201, 100, 66, .06);\n}\n/*\n * Pinned-context zone: whatever a host pins above the composer input (selected plugins, MCP\n * servers, or anything a future host adds -- see 'leadingAccessories''s own doc in 'slots.ts').\n * 'Composer.tsx' only mounts this element when 'slots.leadingAccessories' is a truthy prop value,\n * but a host that always supplies a TRAY COMPONENT rather than conditionally supplying the prop\n * itself (one that internally renders 'null' when nothing is pinned) still leaves an empty node\n * behind -- ':empty' below is what actually collapses THAT case to zero height and zero seam,\n * mirroring '.jini-attachment-tray:empty' a few rules down, which solves the identical problem for\n * the real file-attachment tray.\n *\n * Collapsed (nothing pinned): zero height, zero padding, invisible (0-width) seam -- no reserved\n * space, matching the owner's \"if nil, it doesn't animate\" requirement exactly, since there is\n * nothing here to animate.\n *\n * Populated: grows to a single fixed-height row via 'max-height' (not 'height: auto', which can't\n * transition) and gains a hairline seam using the SAME border/token '.jini-composer-footer' below\n * already uses one zone down -- reads as one more zone of the same control, not a new floating\n * box. The transition runs both directions, so removing the last pinned item gets a matching exit,\n * not an instant cut.\n *\n * 'overflow-x: auto' turns this into a single scrollable row instead of the wrap-and-grow a naive\n * flex row would do with six or more items; '> *' keeps any direct host child (a single tray\n * wrapper, a bare row of buttons, anything) from shrinking to fit, so overflow -- not internal\n * wrapping -- is what kicks in once content is wider than the zone.\n *\n * The \"more content\" cue is a real shadow/vignette, not a plain alpha fade -- an alpha fade toward\n * this zone's own background was tried first and rejected on review: the chip and the zone\n * background are already close in luminance, so fading a chip's OWN pixels toward transparent read\n * as \"something is clipped/broken\", not \"swipe for more\", at a glance with no hover. The four-layer\n * 'background' below is the standard scroll-shadow technique instead: a real 'color-mix'-tinted\n * vignette (darkens in a light theme, lightens in a dark one, since it's mixed from the theme's OWN\n * text color rather than a fixed hex) drawn at each edge -- but each 'scroll'-attached shadow layer\n * is covered by a same-edge 'local'-attached solid-to-transparent layer that scrolls WITH the\n * content, so the shadow is only ever visible on the edges that still have more to reveal: at rest\n * (scrolled to the start) the left shadow is hidden and the right one shows; scroll all the way to\n * the true end and the right shadow hides too. Verified live (not just by reading the technique):\n * scrolling this element from 0 to its max scrollLeft measurably fades the right cover's computed\n * background-position across the full track and the shadow disappears exactly at the end.\n */\n.jini-chat-pane .jini-composer-leading {\n display: flex;\n align-items: center;\n overflow-x: auto;\n overflow-y: hidden;\n max-height: 0;\n padding: 0 12px;\n border-bottom: 0 solid var(--jini-chat-border-soft);\n opacity: 0;\n scrollbar-width: thin;\n transition:\n max-height .22s cubic-bezier(.3, .1, .25, 1),\n padding .22s cubic-bezier(.3, .1, .25, 1),\n border-bottom-width .22s ease,\n opacity .16s ease;\n background-color: var(--jini-chat-subtle);\n background-repeat: no-repeat;\n background-attachment: local, local, scroll, scroll;\n background-position: 0 0, 100% 0, 0 0, 100% 0;\n background-size: 26px 100%, 26px 100%, 20px 100%, 20px 100%;\n background-image:\n linear-gradient(to right, var(--jini-chat-subtle), transparent),\n linear-gradient(to left, var(--jini-chat-subtle), transparent),\n linear-gradient(to right, color-mix(in srgb, var(--jini-chat-text) 30%, transparent), transparent),\n linear-gradient(to left, color-mix(in srgb, var(--jini-chat-text) 30%, transparent), transparent);\n}\n.jini-chat-pane .jini-composer-leading:not(:empty) {\n max-height: 64px;\n padding: 10px 12px;\n border-bottom-width: 1px;\n opacity: 1;\n}\n.jini-chat-pane .jini-composer-leading > * {\n flex: none;\n min-width: max-content;\n}\n@media (prefers-reduced-motion: reduce) {\n .jini-chat-pane .jini-composer-leading { transition: none; }\n}\n.jini-chat-pane .jini-attachment-tray {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n max-height: 88px;\n overflow-y: auto;\n padding: 8px 10px 0;\n scrollbar-width: thin;\n}\n.jini-chat-pane .jini-attachment-tray:empty { display: none; }\n/*\n * The sanctioned reuse path (see '.jini-composer-leading''s own doc above and 'AttachmentTray''s\n * module doc): a host nesting Jini's own tray/chip classes inside the pinned-context zone to match\n * the real attachment tray's look. Un-does the wrap-and-grow rule directly above -- meant for that\n * tray's OWN use one zone down, where wrapping onto a second line is fine -- so the OUTER zone's\n * single scroll axis is the one that wins here instead. The real file-attachment tray is\n * unaffected: it always renders as this zone's sibling ('Composer.tsx'), never its descendant.\n */\n.jini-chat-pane .jini-composer-leading .jini-attachment-tray {\n flex-wrap: nowrap;\n max-height: none;\n overflow: visible;\n padding: 0;\n}\n/* Half the tray's width, so a sidebar-width pane fits two chips per row and its 88px cap shows two\n rows (four chips) before scrolling; at full width each chip took a row and the third was hidden. */\n.jini-chat-pane .jini-attachment-chip {\n display: inline-flex;\n align-items: center;\n box-sizing: border-box;\n min-width: 0;\n max-width: min(calc(50% - 3px), 290px);\n height: 36px;\n padding: 3px 4px 3px 5px;\n color: var(--jini-chat-text);\n background: color-mix(in srgb, var(--jini-chat-panel) 88%, var(--jini-chat-subtle));\n border: 1px solid var(--jini-chat-border);\n border-radius: 9px;\n box-shadow: 0 1px 1px rgba(13, 12, 10, .025);\n animation: jini-chat-chip-in .16s ease both;\n}\n.jini-chat-pane .jini-composer-leading .jini-attachment-chip { max-width: min(100%, 290px); }\n@keyframes jini-chat-chip-in {\n from { opacity: 0; transform: translateY(3px) scale(.94); }\n to { opacity: 1; transform: none; }\n}\n@media (prefers-reduced-motion: reduce) {\n .jini-chat-pane .jini-attachment-chip { animation: none; }\n}\n.jini-chat-pane .jini-attachment-chip-body {\n display: flex;\n align-items: center;\n min-width: 0;\n gap: 7px;\n /* A button (opens the preview modal), reset to the chip's own look. */\n padding: 0;\n font: inherit;\n color: inherit;\n text-align: left;\n background: none;\n border: 0;\n border-radius: 7px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-attachment-chip-body:focus-visible {\n outline: 2px solid var(--jini-chat-accent);\n outline-offset: 2px;\n}\n/* A staged image this browser holds: the thumbnail takes the icon tile's place, same size. */\n.jini-chat-pane .jini-attachment-chip-thumb {\n display: block;\n flex: 0 0 auto;\n width: 26px;\n height: 26px;\n object-fit: cover;\n border-radius: 7px;\n background: var(--jini-chat-subtle);\n}\n.jini-chat-pane .jini-attachment-chip-icon {\n display: grid;\n flex: 0 0 auto;\n place-items: center;\n width: 26px;\n height: 26px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-subtle);\n border-radius: 7px;\n}\n.jini-chat-pane .jini-attachment-chip-icon.is-image {\n color: var(--jini-chat-accent);\n background: var(--jini-chat-accent-soft);\n}\n.jini-chat-pane .jini-attachment-chip-copy {\n display: flex;\n min-width: 0;\n flex-direction: column;\n line-height: 1.15;\n}\n.jini-chat-pane .jini-attachment-chip-name {\n overflow: hidden;\n font-size: 11.5px;\n font-weight: 550;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-attachment-chip-size {\n color: var(--jini-chat-muted);\n font-size: 9.5px;\n}\n.jini-chat-pane .jini-attachment-remove {\n display: grid;\n flex: 0 0 auto;\n place-items: center;\n width: 25px;\n height: 25px;\n margin-left: 4px;\n padding: 0;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 0;\n border-radius: 7px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-attachment-remove:hover,\n.jini-chat-pane .jini-attachment-remove:focus-visible {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-subtle);\n outline: none;\n}\n/* Phones: the 25px \"x\" gets an invisible 44px tap area; the chip keeps its size. The area ends at the\n chip's bottom edge and grows up into the tray's 8px top padding / row gap, because growing down\n past the last row would make the 88px tray scroll by a few pixels. Rows are 8px apart here, not\n 6: at a 42px row pitch the second row's area covered the bottom 2.5px of the first row's, leaving\n the top row a 41.5px target. 8px top padding + two 36px rows + an 8px gap is exactly the 88px cap. */\n@media (max-width: 640px), (pointer: coarse) {\n .jini-chat-pane .jini-attachment-tray { row-gap: 8px; }\n .jini-chat-pane .jini-attachment-remove { position: relative; }\n .jini-chat-pane .jini-attachment-remove::before {\n content: '';\n position: absolute;\n bottom: -5.5px;\n left: 50%;\n width: 44px;\n height: 44px;\n transform: translateX(-50%);\n }\n}\n.jini-chat-pane .jini-composer-input {\n display: block;\n box-sizing: border-box;\n width: 100%;\n min-height: 92px;\n resize: none;\n padding: 12px 14px 8px;\n color: var(--jini-chat-text);\n background: transparent;\n border: 0;\n outline: 0;\n font-size: 13.5px;\n line-height: 1.6;\n}\n/* Shared focus-zoom floor: embeds at every width, all panes on small/touch viewports. */\n.jini-chat-embed .jini-chat-pane .jini-composer-input { font-size: 16px; }\n@media (max-width: 640px), (pointer: coarse) {\n .jini-chat-pane.jini-chat-pane .jini-composer-input { font-size: 16px; }\n}\n.jini-chat-pane .jini-composer-input::placeholder { color: var(--jini-chat-faint); }\n.jini-chat-pane .jini-composer-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n min-height: 42px;\n gap: 6px;\n padding: 6px 8px 8px;\n border-top: 1px solid var(--jini-chat-border-soft);\n}\n.jini-chat-pane .jini-composer-attachment-picker { display: inline-flex; }\n/* Host controls slotted at the start of the footer row, right after the attach/discovery \"+\"\n trigger above -- see ComposerSlots.footerLeadingAccessory's own doc (slots.ts) for why this\n exists as a third footer slot alongside footerAccessories/plusMenuItems. */\n.jini-chat-pane .jini-composer-footer-leading { display: inline-flex; align-items: center; }\n/* NOT 'position: relative' \u2014 the discovery popover (below) anchors off '.jini-composer' itself\n (the nearest positioned ancestor once this wrapper opts out), not off this small trigger-button\n wrapper. Anchoring to the wrapper put the popover's bottom edge at the wrapper's own top edge \u2014\n inside the footer, just below the textarea \u2014 so any popover taller than a couple of rows grew\n upward straight over the textarea and blocked clicks into it (reproduced live: Playwright's own\n click on the textarea failed with \"intercepts pointer events\" while the popover was open). */\n.jini-chat-pane .jini-composer-discovery { display: inline-flex; }\n.jini-chat-pane .jini-composer-discovery-menu,\n.jini-chat-pane .jini-composer-slash-menu {\n position: absolute;\n z-index: 8;\n display: flex;\n flex-direction: column;\n gap: 4px;\n max-height: 280px;\n overflow-y: auto;\n padding: 8px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 10px;\n box-shadow: 0 12px 30px rgb(0 0 0 / 14%);\n}\n/* Both anchor off '.jini-composer' ('bottom: calc(100% + 6px)' = fully above the textarea AND the\n footer, not just above the trigger button) so the popover floats as a clean detached card instead\n of overlapping the input it sits next to. */\n.jini-chat-pane .jini-composer-discovery-menu {\n inset-inline-start: 8px;\n bottom: calc(100% + 6px);\n width: min(280px, calc(100vw - 32px));\n}\n.jini-chat-pane .jini-composer-slash-menu {\n inset-inline: 8px;\n bottom: calc(100% + 6px);\n}\n.jini-chat-pane .jini-composer-discovery-group { display: flex; flex-direction: column; gap: 2px; }\n.jini-chat-pane .jini-composer-discovery-group-label {\n padding: 6px 8px 2px;\n color: var(--jini-chat-faint);\n font-size: 10px;\n font-weight: 700;\n letter-spacing: .08em;\n text-transform: uppercase;\n}\n.jini-chat-pane .jini-composer-discovery-item {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 2px;\n width: 100%;\n padding: 7px 8px;\n color: inherit;\n text-align: start;\n background: transparent;\n border: 0;\n border-radius: 7px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-composer-discovery-item:hover {\n background: var(--jini-chat-panel);\n}\n.jini-chat-pane .jini-composer-discovery-item.is-active {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-accent-soft);\n outline: 1px solid var(--jini-chat-accent);\n outline-offset: -1px;\n box-shadow: inset 3px 0 0 var(--jini-chat-accent);\n}\n.jini-chat-pane .jini-composer-discovery-item small { color: var(--jini-chat-muted); }\n/* The full description used to print under every row unconditionally, which is why the palette\n grew to dozens of multi-line rows on a bare \"/\" \u2014 kept in the accessibility tree at rest (never\n 'display: none', which most screen readers drop from that tree) so 'aria-describedby' keeps\n resolving whether or not anything is hovering.\n FIXED (was: opacity-only toggle, THEN an absolutely-positioned overlay) \u2014 two owner-reported\n bugs in sequence on the same element:\n 1. 'opacity: 0' alone hides a box visually but does not remove it from layout, so an\n absolutely-positioned tooltip sized to its full natural content height (routinely 50-260px\n for a real description) still counted toward its scrolling ancestor's scrollable overflow\n region while invisible ('scrollHeight' measured 501px against a 242px 'clientHeight' for a\n 6-row list), leaving a blank scrollable gap after the last real row. Fixed by collapsing the\n box itself at rest ('max-height: 0; overflow: hidden;'), not just hiding it \u2014 carried\n forward below.\n 2. That fix still left this 'position: absolute; top: 100%' of its own row (formerly anchored\n via 'position: relative' on '.jini-composer-discovery-item' above, now removed \u2014 nothing\n anchors off it anymore): 'z-index' only controls paint order, not which box a box is\n confined to, so on hover/focus the expanded tooltip painted directly over whichever row\n happened to sit below it in the SAME scrollable list (owner report: hovering \"UI/UX Design\n (Skill)\" covered the \"/mcp\" entry beneath it \u2014 not a stacking-order problem, a containment\n one). Fixed by dropping 'position: absolute' entirely: this is now a normal in-flow child of\n the row's own 'flex-direction: column' box, still collapsed to negligible size at rest via\n 'max-height: 0; overflow: hidden;', but on hover/focus it grows the ROW's own height instead\n of floating a detached box over the next row \u2014 the popover's flex column (and every row\n below) gets pushed down to make room, so it can never occlude a sibling item. This also\n retires the panel-styled \"floating card\" look (background/border/shadow): an in-flow caption\n reads correctly with the row's own muted small-text convention above, and dropping it removes\n the last reason this needed its own stacking context. 'pointer-events: none' is kept for\n parity even though nothing sits under it to protect anymore. */\n.jini-chat-pane .jini-composer-discovery-description {\n width: 100%;\n max-height: 0;\n padding: 0;\n overflow: hidden;\n color: var(--jini-chat-muted);\n font-size: 12px;\n font-weight: 400;\n line-height: 1.4;\n white-space: normal;\n opacity: 0;\n pointer-events: none;\n transition: opacity .12s ease;\n}\n.jini-chat-pane .jini-composer-discovery-item:hover .jini-composer-discovery-description,\n.jini-chat-pane .jini-composer-discovery-item:focus-visible .jini-composer-discovery-description {\n max-height: none;\n padding-top: 2px;\n overflow: visible;\n opacity: 1;\n}\n/* '<code>'/'<small>' both default to the browser's UA styling (monospace at an unrelated size)\n with nothing here overriding it \u2014 computed font-SIZE happened to already match the row label\n (13.5px), so the visible mismatch is font-FAMILY: raw monospace next to the label's sans-serif\n stack reads as a different, larger type scale even at equal size. Inheriting the row's own font\n puts both on one consistent scale. */\n.jini-chat-pane .jini-composer-slash-argument,\n.jini-chat-pane .jini-composer-slash-confirm-badge {\n font-family: inherit;\n font-size: inherit;\n font-weight: 400;\n color: var(--jini-chat-muted);\n}\n.jini-chat-pane .jini-composer-file-input {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n clip-path: inset(50%);\n}\n.jini-chat-pane .jini-composer-attach {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n padding: 0;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 1px solid transparent;\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-composer-attach:hover:not(:disabled) {\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border-color: var(--jini-chat-border);\n}\n.jini-chat-pane .jini-composer-attach:disabled { opacity: .45; cursor: default; }\n/* The working-directory trigger button reuses '.jini-composer-attach' itself (Composer.tsx) rather\n than a class duplicated here, so it is byte-identical in size/hit-area/hover/focus to its\n neighbour rather than an approximation that could drift out of sync. Only the wrapper and its\n popover are new. NOT 'position: relative' on the wrapper, for the same reason noted above\n '.jini-composer-discovery': the popover's un-measured first paint (before the layout effect sets\n its 'position: fixed' inline style) falls back to this rule's 'position: absolute', which needs\n '.jini-composer' itself \u2014 the nearest positioned ancestor \u2014 not this wrapper, to anchor against. */\n.jini-chat-pane .jini-composer-workdir { display: inline-flex; }\n.jini-chat-pane .jini-composer-workdir-panel {\n position: absolute;\n z-index: 8;\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px;\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 10px;\n box-shadow: 0 12px 30px rgb(0 0 0 / 14%);\n}\n.jini-chat-pane .jini-composer-workdir-input {\n flex: 1;\n min-width: 160px;\n padding: 6px 8px;\n color: var(--jini-chat-text);\n font: inherit;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 6px;\n}\n.jini-chat-pane .jini-composer-workdir-confirm {\n padding: 6px 10px;\n color: var(--jini-chat-panel);\n white-space: nowrap;\n background: var(--jini-chat-text-strong);\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n/* `.jini-icon-spin` is `<Icon name=\"spinner\">` (the tool-row \"Running\" badge). Its keyframes used to\n live only in reference.css, which no host imports, so the badge sat still. */\n.jini-composer-spinner,\n.jini-runtime-spinner,\n.jini-icon-spin { animation: jini-chat-spin .8s linear infinite; }\n@keyframes jini-chat-spin { to { transform: rotate(360deg); } }\n/* The model/effort cluster sits at the trailing edge beside send (owner, 2026-10-18); the \"+\"\n stays at the start. The auto margin takes the free space, and gives it back first when a narrow\n bar needs the room for the model label. */\n.jini-chat-pane .jini-composer-footer-accessories {\n display: flex;\n min-width: 0;\n margin-left: auto;\n}\n/* Basis 0: the hint takes only the room the runtime row leaves and wraps into it, instead of\n claiming its one-line width and squeezing the picker under its icon/effort labels. */\n.jini-chat-pane .jini-composer-send-blocker {\n flex: 1 1 0;\n align-self: center;\n margin-right: 8px;\n color: var(--jini-chat-muted);\n font-size: 12px;\n}\n/* Always pushed to the trailing edge. This also covers a footer with no attach/accessory controls\n before the send group (a public visitor chat), which would otherwise leave the lone send button\n at the start under the footer's space-between. */\n.jini-chat-pane .jini-composer-send-group {\n display: flex;\n flex-shrink: 0;\n gap: 6px;\n margin-left: auto;\n}\n/* After the cluster, the send group sits right beside it instead of splitting the free space. */\n.jini-chat-pane .jini-composer-footer-accessories ~ .jini-composer-send-group { margin-left: 0; }\n/* Inverse ink: the fill is the strong text colour, so the icon takes the panel colour rather than a\n fixed white. A host dark theme flips text-strong to near-white, and fixed white icons then drew a\n blank white square (owner report, 2026-10-21). Disabled uses the theme's subtle/faint pair too. */\n.jini-chat-pane .jini-composer-send {\n display: grid;\n place-items: center;\n width: 32px;\n height: 32px;\n padding: 0;\n color: var(--jini-chat-panel);\n background: var(--jini-chat-text-strong);\n border: 0;\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-composer-send:disabled {\n color: var(--jini-chat-faint);\n background: var(--jini-chat-subtle);\n cursor: default;\n}\n.jini-chat-pane__workdir {\n display: flex;\n align-items: center;\n gap: 7px;\n min-width: 0;\n padding: 5px 4px 0;\n color: var(--jini-chat-muted);\n font-size: 13px;\n}\n.jini-chat-pane__workdir code {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-working-dir-picker {\n position: relative;\n display: inline-flex;\n align-items: flex-start;\n}\n.jini-chat-pane .jini-working-dir-trigger-row {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n}\n.jini-chat-pane .jini-working-dir-trigger {\n appearance: none;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 6px;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 0;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-working-dir-trigger:hover,\n.jini-chat-pane .jini-working-dir-trigger:focus-visible {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-subtle);\n outline: none;\n}\n.jini-chat-pane .jini-working-dir-trigger.invalid { color: var(--danger); }\n.jini-chat-pane .jini-working-dir-trigger-icon { flex: 0 0 auto; opacity: .75; }\n.jini-chat-pane .jini-working-dir-trigger.invalid .jini-working-dir-trigger-icon {\n color: var(--danger);\n opacity: 1;\n}\n.jini-chat-pane .jini-working-dir-trigger-label {\n max-width: 240px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-working-dir-trigger-chevron { flex: 0 0 auto; opacity: .55; }\n.jini-chat-pane .jini-working-dir-panel,\n.jini-chat-pane .jini-working-dir-flyout {\n position: absolute;\n z-index: 90;\n display: flex;\n flex-direction: column;\n min-width: 210px;\n padding: 5px;\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border-strong);\n border-radius: 12px;\n box-shadow: 0 1px 2px rgba(0, 0, 0, .04), 0 10px 28px -10px rgba(0, 0, 0, .18);\n}\n.jini-chat-pane .jini-working-dir-panel {\n top: calc(100% + 6px);\n left: 0;\n}\n.jini-chat-pane .jini-working-dir-panel.up {\n top: auto;\n bottom: calc(100% + 6px);\n}\n.jini-chat-pane .jini-working-dir-item {\n appearance: none;\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n min-height: 32px;\n padding: 0 9px;\n color: var(--jini-chat-text-strong);\n background: transparent;\n border: 0;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n text-align: left;\n cursor: pointer;\n}\n.jini-chat-pane .jini-working-dir-item:hover,\n.jini-chat-pane .jini-working-dir-item:focus-visible,\n.jini-chat-pane .jini-working-dir-recent-item:hover,\n.jini-chat-pane .jini-working-dir-recent-item:focus-visible {\n background: var(--jini-chat-subtle);\n outline: none;\n}\n.jini-chat-pane .jini-working-dir-item-icon { flex: 0 0 auto; opacity: .8; }\n.jini-chat-pane .jini-working-dir-item > span {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-working-dir-item-chevron { margin-left: auto; opacity: .55; }\n.jini-chat-pane .jini-working-dir-submenu-row { position: relative; }\n.jini-chat-pane .jini-working-dir-flyout {\n top: -5px;\n left: 100%;\n z-index: 91;\n min-width: 220px;\n max-width: 320px;\n margin-left: 4px;\n}\n.jini-chat-pane .jini-working-dir-flyout.up { top: auto; bottom: -5px; }\n.jini-chat-pane .jini-working-dir-flyout::before {\n position: absolute;\n top: 0;\n left: -8px;\n width: 8px;\n height: 100%;\n content: \"\";\n}\n.jini-chat-pane .jini-working-dir-recent-item {\n appearance: none;\n display: grid;\n grid-template-rows: auto auto;\n grid-template-columns: auto 1fr;\n align-items: center;\n width: 100%;\n min-height: 36px;\n padding: 4px 9px;\n color: var(--jini-chat-text-strong);\n background: transparent;\n border: 0;\n border-radius: 8px;\n font: inherit;\n text-align: left;\n cursor: pointer;\n column-gap: 9px;\n}\n.jini-chat-pane .jini-working-dir-recent-item .jini-working-dir-item-icon {\n grid-row: 1 / span 2;\n}\n.jini-chat-pane .jini-working-dir-recent-name,\n.jini-chat-pane .jini-working-dir-recent-path {\n grid-column: 2;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-chat-pane .jini-working-dir-recent-name { font-size: 13px; }\n.jini-chat-pane .jini-working-dir-recent-path {\n color: var(--jini-chat-muted);\n font-size: 11px;\n opacity: .7;\n}\n.jini-chat-pane .jini-working-dir-empty {\n padding: 8px 10px;\n color: var(--jini-chat-muted);\n font-size: 12px;\n white-space: nowrap;\n opacity: .7;\n}\n/* Composer-bar runtime row: [agent icon] [model \u2304] [effort \u2304] -- compact, borderless, muted text.\n The icon opens the runtime popover; model and effort each open a drop-up (RuntimeDropUp). */\n.jini-runtime-picker {\n position: relative;\n display: flex;\n align-items: center;\n min-width: 0;\n}\n.jini-runtime-trigger,\n.jini-runtime-dropup-trigger {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n height: 32px;\n min-width: 0;\n padding: 3px 6px;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 0;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n cursor: pointer;\n}\n.jini-runtime-trigger { gap: 6px; padding: 3px 4px; }\n/* Local mode's trigger is the icon alone, so it has nothing to give way: shrunk under the shared\n `min-width: 0` on a narrow bar, the 22px icon spilled over the model/effort labels beside it\n (owner report, 2026-10-21). API mode keeps shrinking -- its model label ellipsizes. */\n.jini-runtime-trigger--icon-only { flex: none; }\n.jini-runtime-trigger:hover,\n.jini-runtime-dropup-trigger:hover,\n.jini-runtime-dropup-trigger[aria-expanded=\"true\"] { color: var(--jini-chat-text); background: var(--jini-chat-subtle); }\n.jini-runtime-trigger:focus-visible,\n.jini-runtime-dropup-trigger:focus-visible { outline: 2px solid var(--jini-chat-accent); outline-offset: 1px; }\n/* 'contents' so the hidden name (absolutely positioned) adds no flex gap beside the icon-only\n local trigger, and the API-mode model label is a direct, shrinkable flex item. */\n.jini-runtime-trigger__copy { display: contents; }\n/* Kept in the text (see the component), not shown: the icon already names the agent. */\n.jini-runtime-trigger__copy strong {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0 0 0 0);\n white-space: nowrap;\n}\n.jini-runtime-trigger__copy small,\n.jini-runtime-dropup-trigger__label {\n overflow: hidden;\n max-width: 180px;\n font-size: inherit;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-runtime-trigger__chevron { flex: none; }\n/* The effort label is short; the model label is the one that gives way on a narrow bar. */\n.jini-runtime-dropup-trigger--effort { flex: none; }\n.jini-runtime-dropup {\n display: flex;\n flex-direction: column;\n gap: 1px;\n padding: 4px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 10px;\n box-shadow: var(--jini-chat-popover-shadow, 0 18px 46px rgba(13, 12, 10, .15), 0 2px 8px rgba(13, 12, 10, .06));\n font-size: 13px;\n line-height: 1.4;\n}\n.jini-runtime-dropup__note {\n padding: 4px 10px 6px;\n color: var(--jini-chat-muted);\n font-size: 11.5px;\n}\n.jini-runtime-dropup__item {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n min-height: 32px;\n padding: 6px 10px;\n color: inherit;\n background: transparent;\n border: 0;\n border-radius: 6px;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.jini-runtime-dropup__item:hover,\n.jini-runtime-dropup__item:focus-visible { background: var(--jini-chat-subtle); outline: none; }\n.jini-runtime-dropup__item.is-active { font-weight: 600; }\n.jini-runtime-dropup__item-label { flex: 1; min-width: 0; overflow-wrap: anywhere; }\n.jini-runtime-agent-icon.agent-icon { flex: 0 0 auto; object-fit: contain; }\n.jini-runtime-agent-icon.agent-icon-mono {\n display: inline-block;\n background-color: currentColor;\n -webkit-mask-repeat: no-repeat;\n mask-repeat: no-repeat;\n -webkit-mask-position: center;\n mask-position: center;\n -webkit-mask-size: contain;\n mask-size: contain;\n}\n.jini-runtime-agent-icon.agent-icon-fallback {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: #74716b;\n background: #f4f5f7;\n border: 1px solid #e1e5eb;\n border-radius: 6px;\n font-weight: 700;\n}\n.jini-runtime-popover {\n --jini-chat-panel: #fdfcfa;\n --jini-chat-text: #1a1916;\n --jini-chat-text-strong: #0d0c0a;\n --jini-chat-muted: #74716b;\n --jini-chat-faint: #b3b0a8;\n --jini-chat-border: #e1e5eb;\n --jini-chat-border-soft: #edf0f4;\n --jini-chat-subtle: #f4f5f7;\n box-sizing: border-box;\n padding: 8px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 12px;\n box-shadow: var(--jini-chat-popover-shadow, 0 18px 46px rgba(13, 12, 10, .15), 0 2px 8px rgba(13, 12, 10, .06));\n font-family: var(--jini-chat-font-body, -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Microsoft YaHei UI\", \"Noto Sans\", Roboto, \"Helvetica Neue\", Arial, sans-serif);\n font-size: 13.5px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n}\n.jini-runtime-popover__head {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 10px 12px 12px;\n border-bottom: 1px solid var(--jini-chat-border-soft);\n}\n.jini-runtime-popover__head strong { color: var(--jini-chat-text-strong); font-size: 14px; font-weight: 650; }\n.jini-runtime-popover__head span { color: var(--jini-chat-muted); font-size: 12px; }\n.jini-runtime-mode,\n.jini-runtime-agent,\n.jini-runtime-rescan {\n display: flex;\n align-items: center;\n gap: 11px;\n box-sizing: border-box;\n width: 100%;\n min-height: 40px;\n padding: 8px 11px;\n color: var(--jini-chat-text);\n background: transparent;\n border: 0;\n border-radius: 8px;\n text-align: left;\n cursor: pointer;\n}\n.jini-runtime-mode:hover,\n.jini-runtime-agent:hover:not(:disabled),\n.jini-runtime-rescan:hover:not(:disabled) { background: var(--jini-chat-subtle); }\n.jini-runtime-mode.is-active { font-weight: 650; }\n.jini-runtime-mode:disabled,\n.jini-runtime-agent:disabled,\n.jini-runtime-rescan:disabled { color: var(--jini-chat-faint); cursor: default; }\n.jini-runtime-mode__meta,\n.jini-runtime-agent__status {\n margin-left: auto;\n color: var(--jini-chat-muted);\n font-size: 12px;\n white-space: nowrap;\n}\n.jini-runtime-check { flex: 0 0 auto; color: var(--jini-chat-text); }\n/* Matches .jini-message-attachment-chip/.jini-md-table-expand's pill chrome (same\n background/border/radius formula) so this reads as the same badge language already used\n elsewhere in the pane, not a new one. Sits between the agent name and its status column \u2014\n flex: 0 0 auto keeps it from stretching or shrinking when the row is tight. */\n.jini-runtime-agent__badge {\n flex: 0 0 auto;\n padding: 2px 8px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n font-size: 10px;\n font-weight: 500;\n white-space: nowrap;\n}\n.jini-runtime-section-label {\n padding: 12px 11px 5px;\n color: var(--jini-chat-faint);\n font-size: 11px;\n font-weight: 650;\n letter-spacing: .08em;\n text-transform: uppercase;\n}\n.jini-runtime-agent.is-active { background: transparent; }\n.jini-runtime-agent__copy {\n display: flex;\n flex: 1;\n flex-direction: column;\n min-width: 0;\n}\n.jini-runtime-agent__copy strong { font-size: 13.5px; font-weight: 500; }\n.jini-runtime-agent__copy small {\n overflow: hidden;\n color: #8b857e;\n font-size: 10px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.jini-runtime-empty { padding: 10px 11px; color: var(--jini-chat-muted); font-size: 12px; }\n.jini-runtime-models {\n display: flex;\n flex-direction: column;\n gap: 9px;\n margin-top: 5px;\n padding: 10px 11px 8px;\n border-top: 1px dashed var(--jini-chat-border);\n}\n.jini-runtime-select {\n display: flex;\n flex-direction: column;\n gap: 5px;\n color: var(--jini-chat-muted);\n font-size: 13px;\n}\n.jini-runtime-select select {\n box-sizing: border-box;\n width: 100%;\n min-height: 40px;\n padding: 7px 10px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n}\n/*\n * The BYOK model, rendered as a value rather than a control \u2014 see RuntimeByokDetails for why it\n * is not a disabled select. It occupies the same box the sibling selects do so the row still\n * reads as part of the same list, but carries no border: a bordered box with no affordance is\n * exactly the \"looks editable, isn't\" shape the plain text exists to avoid.\n */\n/*\n * The BYOK model list, which CustomSelect portals to document.body. runtimePopoverPosition puts\n * the runtime popover at z-index 1000; the shared .jini-select-menu default is 60, which is right\n * everywhere else and puts this menu behind the very popover that opened it. One above the\n * popover, not far above: it must clear its own surface and nothing else.\n */\n.jini-select-menu.jini-runtime-model-menu {\n z-index: 1001;\n}\n.jini-runtime-byok-model strong {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n min-height: 40px;\n padding: 7px 10px;\n color: var(--jini-chat-text);\n font-weight: 600;\n word-break: break-all;\n}\n/*\n * Tool-call cards (ToolCard.tsx) and the per-turn usage summary (MessageRow.tsx) ship no CSS of\n * their own \u2014 see this file's own header note on why the package supplies a default theme at all.\n * Collapsed by default (.op-card-head is the only always-visible part), so a run with a dozen tool\n * calls reads as a dozen one-line rows, not a page of raw JSON.\n */\n.jini-chat-pane .jini-message-tools {\n display: flex;\n flex-direction: column;\n gap: 6px;\n width: 100%;\n}\n.jini-chat-pane .op-card {\n border: 1px solid var(--jini-chat-border);\n border-radius: var(--radius);\n background: var(--jini-chat-subtle);\n overflow: hidden;\n font-size: 13px;\n}\n.jini-chat-pane .op-card-head {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 7px 10px;\n background: none;\n border: 0;\n font: inherit;\n text-align: left;\n cursor: pointer;\n color: inherit;\n}\n.jini-chat-pane .op-status { display: inline-flex; flex-shrink: 0; }\n.jini-chat-pane .op-status-ok { color: #4e875f; }\n.jini-chat-pane .op-status-error { color: var(--danger); }\n.jini-chat-pane .op-status-running { color: var(--jini-chat-accent); }\n.jini-chat-pane .op-title { font-weight: 600; flex-shrink: 0; }\n.jini-chat-pane .shimmer-text { animation: jini-chat-shimmer 1.6s ease-in-out infinite; }\n@keyframes jini-chat-shimmer { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }\n@media (prefers-reduced-motion: reduce) { .jini-chat-pane .shimmer-text { animation: none; opacity: .6; } }\n.jini-chat-pane .op-meta {\n color: var(--jini-chat-muted);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: 1;\n min-width: 0;\n}\n.jini-chat-pane .op-expand-chev { flex-shrink: 0; color: var(--jini-chat-faint); }\n/* Collapsed by default: 0 max-height clips the inner content, no display:none \u2014 keeps the height\n transition instead of an instant jump cut. */\n.jini-chat-pane .accordion-collapsible { max-height: 0; overflow: hidden; transition: max-height .15s ease; }\n.jini-chat-pane .accordion-collapsible.open { max-height: 480px; overflow-y: auto; }\n.jini-chat-pane .accordion-collapsible-inner { padding: 0 10px 10px; }\n.jini-chat-pane .op-card-detail { display: flex; flex-direction: column; gap: 6px; }\n.jini-chat-pane .op-path,\n.jini-chat-pane .op-command,\n.jini-chat-pane .op-output {\n font-family: var(--jini-chat-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);\n font-size: 12px;\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 6px;\n padding: 6px 8px;\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n}\n/* The \"Waiting for the result or answer\u2026\" / \"Finished with no output.\" line in an expanded tool row (ToolCard.tsx's ToolCallDetail). */\n.jini-chat-pane .op-detail-note { margin: 0; font-size: 12px; color: var(--jini-chat-muted); }\n.jini-chat-pane .op-open {\n border: 1px solid var(--jini-chat-border-strong);\n border-radius: 6px;\n background: var(--jini-chat-panel);\n padding: 2px 8px;\n font-size: 11px;\n cursor: pointer;\n}\n/* The \"Done \u00B7 6m 29s \u00B7 2612 out \u00B7 $0.4028\" line \u2014 real numbers from the run's own kind:'usage' event. */\n.jini-chat-pane .jini-message-usage {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n color: var(--jini-chat-muted);\n margin-top: 2px;\n}\n.jini-chat-pane .jini-message-usage-dot { font-size: 8px; color: #4e875f; }\n/* toolCallDisplay 'collapsed' (ToolCallGroup.tsx): one quiet line per turn, cards open under it. */\n.jini-chat-pane .jini-tool-group { display: flex; flex-direction: column; gap: 6px; width: 100%; }\n.jini-chat-pane .jini-tool-group__toggle {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n align-self: flex-start;\n max-width: 100%;\n padding: 2px 0;\n background: none;\n border: 0;\n font: inherit;\n font-size: 12px;\n color: var(--jini-chat-muted);\n cursor: pointer;\n}\n.jini-chat-pane .jini-tool-group__toggle:hover { color: inherit; }\n.jini-chat-pane .jini-tool-group__toggle:focus-visible { outline: 2px solid var(--jini-chat-accent); outline-offset: 2px; border-radius: 4px; }\n.jini-chat-pane .jini-tool-group__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.jini-chat-pane .jini-tool-group[data-failed=\"true\"] .jini-tool-group__toggle { color: var(--danger); }\n/* showThinking (MessageRow.tsx's ThinkingBlock): the model's reasoning, folded, set apart from the answer. */\n.jini-chat-pane .jini-message-thinking { font-size: 12px; color: var(--jini-chat-muted); }\n.jini-chat-pane .jini-message-thinking > summary { cursor: pointer; width: fit-content; }\n.jini-chat-pane .jini-message-thinking__text {\n margin-top: 4px;\n padding-left: 10px;\n border-left: 2px solid var(--jini-chat-border);\n white-space: pre-wrap;\n word-break: break-word;\n}\n/*\n * Fenced code block ('Markdown.tsx''s 'renderBlock' case 'code') and inline 'code' span visual\n * treatment \u2014 previously entirely unstyled (bare UA-default '<pre><code>', no background, border,\n * or padding; a downstream admin host's own stylesheet already flagged this gap in a note next to\n * its unrelated horizontal-scroll fix for the same element). Lives HERE, in the package's own\n * injected default theme, rather than in a host's stylesheet: a host CAN already remap the exact\n * '--jini-chat-*' custom properties this rule reads (one such host's '.admin-chat-dock .jini-chat-\n * pane' block already does, for every other rule in this file), so putting the fix here makes every\n * Jini host look correct by default AND lets a host match its own palette for free, with no new\n * host-side override rule needed at all \u2014 the reverse (host-only CSS) would leave every other\n * embedder of '@jini-ai/chat' with the same unstyled '<pre>' this fix exists to correct.\n * 'max-width'/'overflow-x' here duplicate (harmlessly) a narrower fix that same host's own\n * '.jini-message-content pre' rule already carries for its own fixed-width dock specifically \u2014 this\n * package cannot assume every host has that rule, so a real host-agnostic default belongs here too.\n * NOTE: this comment avoids backtick quoting on purpose \u2014 this whole stylesheet lives inside a JS\n * template literal, so a literal backtick here would terminate the string, not just this comment.\n */\n.jini-chat-pane .jini-message-content pre {\n margin: 6px 0;\n max-width: 100%;\n overflow-x: auto;\n padding: 10px 12px;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n font-family: var(--jini-chat-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);\n font-size: 12.5px;\n line-height: 1.5;\n}\n.jini-chat-pane .jini-message-content pre code {\n padding: 0;\n background: none;\n border: 0;\n color: inherit;\n font: inherit;\n}\n.jini-chat-pane .jini-message-content :not(pre) > code {\n padding: .15em .4em;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-radius: 4px;\n font-family: var(--jini-chat-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);\n font-size: .9em;\n}\n/*\n * Copy affordance for a fenced code block ('Markdown.tsx''s 'CodeBlock'/'CopyCodeButton'). The\n * button lives in its own toolbar row fused to the TOP of the '<pre>' below, not overlaid on top\n * of the code itself \u2014 an overlay would sit over whatever text happens to render at that corner,\n * and '<pre>' already carries its own horizontal scrollbar along its own bottom edge, so a row\n * above it can never collide with that scrollbar either. '.jini-md-code-block' takes over the\n * '<pre>' rule's own top/bottom margin above so the two don't stack, and the toolbar/pre pair\n * share one rounded box (toolbar keeps the top corners, '<pre>' below loses them) instead of two\n * separately-bordered boxes stacked on each other.\n */\n.jini-chat-pane .jini-message-content .jini-md-code-block {\n margin: 6px 0;\n}\n.jini-chat-pane .jini-message-content .jini-md-code-block pre {\n margin: 0;\n border-top: 0;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.jini-chat-pane .jini-md-code-toolbar {\n display: flex;\n justify-content: flex-end;\n padding: 3px;\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border);\n border-bottom: 0;\n border-radius: 8px 8px 0 0;\n}\n.jini-chat-pane .jini-md-code-copy {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n padding: 0;\n color: var(--jini-chat-faint);\n background: none;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n.jini-chat-pane .jini-md-code-copy:hover { color: var(--jini-chat-muted); background: var(--jini-chat-border); }\n.jini-chat-pane .jini-md-code-copy:focus-visible { outline: 2px solid var(--jini-chat-accent); outline-offset: 1px; }\n/* Visually hidden but present in the DOM (never display:none) so this polite live region still\n gets announced \u2014 same clip-rect technique 'jini-message-copy-status' above already uses. */\n.jini-chat-pane .jini-md-code-copy-status {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n}\n/*\n * GFM pipe tables ('Markdown.tsx''s 'TableBlock') \u2014 same reasoning as the code-block rules directly\n * above for living in the package's own default theme rather than a host stylesheet. '.jini-md-\n * table-wrap' gets its own horizontal scrollbar so a wide table scrolls internally instead of\n * dragging '.jini-message-list' sideways with it (the exact failure mode that same downstream host's\n * stylesheet documents for the code-block case \u2014 the same fix shape applies here). The \"Expand\n * table\" button only renders ('TableBlock''s own logic) once the table has genuinely overflowed\n * that wrap.\n */\n.jini-md-table-wrap {\n max-width: 100%;\n overflow-x: auto;\n}\n.jini-md-table {\n border-collapse: collapse;\n font-size: 12.5px;\n}\n/*\n * 'min-width' here, not on '.jini-md-table' itself, and picked per column rather than as one flat\n * table-wide number -- a flat table minimum sized for a typical 2-column table would leave a\n * 5-column table crushed just as badly as before (same failure this comment's neighbor above\n * describes), while one sized for 5 columns would force pointless overflow on a simple 2-column\n * table. Per-cell 'min-width' scales the effective table floor with the real column count instead.\n * 130px was picked empirically, not guessed: measured (real Chromium, not jsdom -- 'scrollWidth'/\n * 'clientWidth' are unimplemented there) against this package's narrowest known real host, an\n * admin chat dock fixed at 380px wide ('admin-chat-dock' in that host's own stylesheet), which\n * nets out to roughly 238px of usable width once that dock's own message-list padding, the 80%\n * message-bubble cap, and this content's own padding are all applied. Below 120px per column the\n * table still fits inside those 238px and never overflows -- the exact bug this fix exists to\n * correct. 130px was chosen just above that measured 120px crossover (a small margin so it clears\n * reliably rather than sitting exactly on the boundary) and reads as a genuinely defensible per-\n * column floor on its own terms too: with this rule's own 6px/10px cell padding subtracted, it\n * leaves about 110px of text room, roughly 14-16 characters per wrapped line at this rule's 12.5px\n * font -- enough that a table cell wraps in short phrases instead of one word per line, which is\n * the readability floor this fix is actually chasing. One real consequence, verified rather than\n * hand-waved: at this width, ANY 2-column table in that narrow dock now overflows and gets the\n * \"Expand table\" affordance, including ones with short cell content -- there is no per-cell\n * min-width value that can overflow a long-prose 2-column table without also overflowing a short\n * one at the same column count, because the browser's own auto-layout table-width algorithm sizes\n * the table to the greater of its container's width and the sum of each column's own minimum --\n * content length past that minimum only changes how much a column wraps internally, never whether\n * the table exceeds its wrap. Accepted here: in a surface already this narrow, showing \"Expand\n * table\" a little more eagerly is the better failure mode than silently crushing a table's own\n * content illegibly, which is the state this whole rule exists to fix.\n */\n.jini-md-table th,\n.jini-md-table td {\n padding: 6px 10px;\n min-width: 130px;\n border: 1px solid var(--jini-chat-border);\n text-align: left;\n}\n.jini-md-table th {\n background: var(--jini-chat-subtle);\n color: var(--jini-chat-text-strong);\n font-weight: 650;\n}\n.jini-md-table-expand {\n margin-top: 4px;\n padding: 4px 10px;\n color: var(--jini-chat-muted);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 999px;\n font-size: 11px;\n cursor: pointer;\n}\n.jini-md-table-expand:hover { color: var(--jini-chat-text-strong); }\n.jini-md-table-modal {\n max-width: min(90vw, 900px);\n max-height: 80vh;\n overflow: auto;\n padding: 20px;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 12px;\n box-shadow: var(--jini-chat-popover-shadow, 0 18px 46px rgba(13, 12, 10, .15), 0 2px 8px rgba(13, 12, 10, .06));\n}\n.jini-md-table-modal::backdrop {\n background: rgba(0, 0, 0, .4);\n}\n.jini-md-table-modal-close {\n position: absolute;\n top: 10px;\n right: 10px;\n width: 28px;\n height: 28px;\n color: var(--jini-chat-muted);\n background: transparent;\n border: 0;\n border-radius: 6px;\n font-size: 16px;\n line-height: 1;\n cursor: pointer;\n}\n.jini-md-table-modal-close:hover {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-subtle);\n}\n@media (max-width: 560px) {\n .jini-chat-pane__header { padding-inline: 16px; }\n .jini-chat-pane .jini-message-list { padding-inline: 16px; }\n .jini-chat-pane__controls { padding-inline: 10px; }\n}\n/*\n * Attachment preview modal (MessageRow.tsx's chip -> AttachmentPreviewModal.tsx). Same\n * native-<dialog> shape as .jini-md-table-modal above (own box sized to content, not viewport;\n * ::backdrop dims the page), kept as its own class rather than reusing that one because the two\n * differ in intent: a table pop-out is a fixed-chrome expansion of content already on screen, this\n * is a general image/text/metadata viewer with its own header and a dedicated close control.\n */\n.jini-attachment-preview-dialog {\n display: flex;\n flex-direction: column;\n width: min(90vw, 720px);\n max-height: 80vh;\n padding: 0;\n color: var(--jini-chat-text);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 12px;\n box-shadow: var(--jini-chat-popover-shadow, 0 18px 46px rgba(13, 12, 10, .15), 0 2px 8px rgba(13, 12, 10, .06));\n}\n.jini-attachment-preview-dialog::backdrop {\n background: rgba(0, 0, 0, .4);\n}\n.jini-attachment-preview-header {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--jini-chat-border-soft);\n}\n.jini-attachment-preview-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n font-weight: 650;\n color: var(--jini-chat-text-strong);\n}\n.jini-attachment-preview-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 auto;\n width: 26px;\n height: 26px;\n margin-left: auto;\n padding: 0;\n color: var(--jini-chat-muted);\n background: none;\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n}\n.jini-attachment-preview-close:hover {\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-subtle);\n}\n.jini-attachment-preview-body {\n display: flex;\n flex: 1;\n min-height: 0;\n align-items: center;\n justify-content: center;\n padding: 16px;\n overflow: auto;\n}\n.jini-attachment-preview-image {\n display: block;\n max-width: 100%;\n max-height: calc(80vh - 90px);\n object-fit: contain;\n margin: auto;\n}\n.jini-attachment-preview-text {\n /* Preserve this preview's wider fallback stack when the host does not supply a chat font. */\n --jini-chat-font-mono: var(--jini-theme-chat-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);\n width: 100%;\n max-height: calc(80vh - 90px);\n margin: 0;\n padding: 12px;\n overflow: auto;\n color: var(--jini-chat-text);\n background: var(--jini-chat-subtle);\n border: 1px solid var(--jini-chat-border-soft);\n border-radius: 8px;\n font-family: var(--jini-chat-font-mono);\n font-size: 12px;\n line-height: 1.5;\n white-space: pre-wrap;\n word-break: break-word;\n}\n.jini-attachment-preview-truncated {\n width: 100%;\n margin-top: 8px;\n color: var(--jini-chat-muted);\n font-size: 11px;\n text-align: center;\n}\n.jini-attachment-preview-loading {\n color: var(--jini-chat-muted);\n font-size: 13px;\n font-style: italic;\n}\n.jini-attachment-preview-meta {\n display: flex;\n flex-direction: column;\n gap: 6px;\n width: 100%;\n max-width: 360px;\n color: var(--jini-chat-text);\n font-size: 13px;\n}\n.jini-attachment-preview-meta-row {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n}\n.jini-attachment-preview-meta-label {\n color: var(--jini-chat-faint);\n}\n.jini-attachment-preview-meta-note {\n margin-top: 6px;\n color: var(--jini-chat-muted);\n font-size: 12px;\n}\n/* Previous/next across a message's (or the composer's) images: a footer under the image, so the\n * buttons sit on the panel colour - readable over any picture - side by side in the bottom-right. */\n/* A fixed stage while stepping: sized to each image, the dialog would resize and move the buttons\n * out from under the pointer between clicks. */\n.jini-attachment-preview-dialog.has-nav .jini-attachment-preview-body {\n flex: 0 0 auto;\n height: min(calc(80vh - 152px), 480px);\n}\n.jini-attachment-preview-dialog.has-nav .jini-attachment-preview-image,\n.jini-attachment-preview-dialog.has-nav .jini-attachment-preview-text {\n max-height: 100%;\n}\n.jini-attachment-preview-nav {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 10px 14px;\n border-top: 1px solid var(--jini-chat-border-soft);\n}\n.jini-attachment-preview-count {\n margin-right: auto;\n color: var(--jini-chat-muted);\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n}\n.jini-attachment-preview-step {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n padding: 0;\n color: var(--jini-chat-text-strong);\n background: var(--jini-chat-panel);\n border: 1px solid var(--jini-chat-border);\n border-radius: 8px;\n cursor: pointer;\n}\n.jini-attachment-preview-step:hover {\n background: var(--jini-chat-subtle);\n border-color: var(--jini-chat-border-strong);\n}\n.jini-attachment-preview-step:focus-visible {\n outline: 2px solid var(--jini-chat-accent);\n outline-offset: 2px;\n}\n/* Phones: 44px targets. The 26px close \"x\" keeps its size inside an invisible 44px area (centred,\n so it stays within the header's 12px/14px padding); the bordered previous/next buttons just grow. */\n@media (max-width: 640px), (pointer: coarse) {\n .jini-attachment-preview-close { position: relative; }\n .jini-attachment-preview-close::before {\n content: '';\n position: absolute;\n top: 50%;\n left: 50%;\n width: 44px;\n height: 44px;\n transform: translate(-50%, -50%);\n }\n .jini-attachment-preview-step {\n width: 44px;\n height: 44px;\n }\n}\n";
2
2
  //# sourceMappingURL=styles.d.ts.map