dsh-zen-remote 2.2.6 → 2.2.8

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.
@@ -11,12 +11,20 @@ export type MobileAttachButtonProps = PropsRuntime<'conversation.input.left'> &
11
11
  * the CLIENT's picker — no `accept` attribute on purpose, so iOS offers the
12
12
  * full 相册 / 拍照 / 选取文件 sheet rather than one of them.
13
13
  *
14
- * DSH 0.1.5 gave the official tool row its own paperclip (the host attachment
15
- * flow), which made TWO identical controls on a phone. This button now stands
16
- * down entirely while the host ships its own picker (`host-attach.ts` probes
17
- * for it) — the official flow uploads into `~/.dsh/attachments` and renders
18
- * its own rail, so the composer keeps exactly one attach affordance. On a
19
- * host without one (DSH ≤ 0.1.2) everything below loads exactly as before.
14
+ * DSH 0.1.5 gave the official tool row its own paperclip, and this button
15
+ * used to stand down whenever that flow's hidden `<input type="file">` was
16
+ * probed in the composer (that probe file is deleted with #31), keeping one
17
+ * attach control per composer. DSH 0.2.x then removed the standalone
18
+ * paperclip (the file action moved into the `+` command menu) while KEEPING
19
+ * the hidden input, so the probe matched on every host and the phone composer
20
+ * was left with no attach affordance at all. The phone shell now always
21
+ * renders this button — its picker is the CLIENT's (相册 / 拍照 / 选取文件),
22
+ * and every attachment takes the same `@.dsh-uploads/` draft-token path on
23
+ * every DSH version. On DSH ≤ 0.1.5, which still ships the second official
24
+ * paperclip, composer.css.ts hides it below 768px so the row keeps exactly
25
+ * one attach control. Subagent sessions stay buttonless, mirroring the host:
26
+ * the official InputBar disables its own file input while
27
+ * `subagent !== null`.
20
28
  *
21
29
  * Every picked file takes the SAME path: upload to the node half, then append
22
30
  * `@.dsh-uploads/name` to the draft through the official write path — no DOM
@@ -31,5 +39,5 @@ export type MobileAttachButtonProps = PropsRuntime<'conversation.input.left'> &
31
39
  * Files upload one at a time: a phone uplink gains nothing from parallelism
32
40
  * and a serial loop keeps the mentions in pick order.
33
41
  */
34
- export declare function MobileAttachButton({ t, sessionId, useInput, inputActions }: MobileAttachButtonProps): import("react").JSX.Element | null;
42
+ export declare function MobileAttachButton({ t, sessionId, useInput, inputActions, useSessions }: MobileAttachButtonProps): import("react").JSX.Element | null;
35
43
  //# sourceMappingURL=MobileAttachButton.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MobileAttachButton.d.ts","sourceRoot":"","sources":["../../../src/client/MobileAttachButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAGjF,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAIjC,mDAAmD;AACnD,MAAM,MAAM,uBAAuB,GAC/B,YAAY,CAAC,yBAAyB,CAAC,GACvC,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;AAsB1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,EAAE,uBAAuB,sCA+EnG"}
1
+ {"version":3,"file":"MobileAttachButton.d.ts","sourceRoot":"","sources":["../../../src/client/MobileAttachButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAGjF,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGjC,mDAAmD;AACnD,MAAM,MAAM,uBAAuB,GAC/B,YAAY,CAAC,yBAAyB,CAAC,GACvC,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;AAsB1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAE,EAAE,uBAAuB,sCAmFhH"}
@@ -1 +1 @@
1
- {"version":3,"file":"MobileAttachChips.d.ts","sourceRoot":"","sources":["../../../src/client/MobileAttachChips.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAEjF,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAWjC,iDAAiD;AACjD,MAAM,MAAM,sBAAsB,GAC9B,YAAY,CAAC,yBAAyB,CAAC,GACvC,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;AAE1B;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,EAAE,sBAAsB,sCAkDtF"}
1
+ {"version":3,"file":"MobileAttachChips.d.ts","sourceRoot":"","sources":["../../../src/client/MobileAttachChips.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAEjF,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAUjC,iDAAiD;AACjD,MAAM,MAAM,sBAAsB,GAC9B,YAAY,CAAC,yBAAyB,CAAC,GACvC,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;AAE1B;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,EAAE,sBAAsB,sCA4CtF"}
@@ -1,33 +1,37 @@
1
1
  import type { ClientContext } from '../compat/types.ts';
2
2
  /**
3
- * Phone: park the composer row's third-party controls inside the model sheet.
3
+ * Phone: move the composer row's secondary third-party controls into a
4
+ * 26px horizontally-scrollable strip inside the dock rail above the card —
5
+ * always visible, one tap from anywhere, no sheet in between.
4
6
  *
5
- * With the speed chip and the vision toggle both present on a GPT model the
6
- * row runs out of width. Both are model-scoped settings, so the model sheet —
7
- * which already holds 模型 and 推理等级 — is where they belong (user request,
8
- * 2026-09-06).
7
+ * Why move the real controls instead of drawing our own rows that drive
8
+ * them: the same reason native-trigger-overlay.ts moves the real trigger. A
9
+ * stand-in has to script the original, which is a road this plugin does not
10
+ * go down — beyond the trusted-input wall that effect documents, our own
11
+ * rows would have to mirror every piece of state (speed tier, disabled,
12
+ * aria-pressed, whether a vision twin exists for the current model) and
13
+ * would rot on the next upstream restyle. Moving the node keeps one source
14
+ * of truth: their React roots go on owning and updating these elements, we
15
+ * only change where they hang — never cloneNode, and the move is anchored
16
+ * with a Comment at the original spot so `unparkAll` can put each node back
17
+ * exactly where React left it.
9
18
  *
10
- * Why move the real controls instead of drawing our own rows that drive them:
11
- * the same reason native-trigger-overlay.ts moves the real trigger. A stand-in
12
- * has to script the original, which is a road this plugin does not go down —
13
- * beyond the trusted-input wall that effect documents, our own rows would have
14
- * to mirror every piece of state (speed tier, disabled, aria-pressed, whether
15
- * a vision twin exists for the current model) and would rot on the next
16
- * upstream restyle. Moving the node keeps one source of truth: their React
17
- * roots go on owning and updating these elements, we only change where they
18
- * hang.
19
+ * Measured 2026-09-06 (DSH 0.1.2, 390px) before the original version of
20
+ * this effect — the two things that would have killed the approach both
21
+ * hold: a relocated control survives host re-renders (typing into the
22
+ * composer re-renders the row and does not yank it back) and stays
23
+ * hit-testable at its new position.
19
24
  *
20
- * Measured 2026-09-06 (DSH 0.1.2, 390px) before writing this — the two things
21
- * that would have killed the approach both hold: a relocated control survives
22
- * host re-renders (typing into the composer re-renders the row and does not
23
- * yank it back) and stays hit-testable at its new position.
24
- *
25
- * Restoring on close is not cosmetic. The sheet unmounts when it closes and
26
- * anything still inside goes with it, so the controls would vanish from the
27
- * page while their React roots kept updating detached nodes. The observer
28
- * therefore watches the menu leaving too, and the parked node is held as a
29
- * reference rather than looked up — once the menu is detached a document
30
- * query can no longer find it.
25
+ * Known hazard, inherent to relocating React-owned nodes: a control whose
26
+ * REGISTRATION unmounts while parked (plugin disabled, slot re-keyed) is
27
+ * removed by React through its ORIGINAL parent — `parent.removeChild(child)`
28
+ * throws when the child moved, and the exception lands in React's commit.
29
+ * Updates that keep the registration mounted (state-driven re-renders,
30
+ * text/attribute changes, a control's own inner nodes unmounting) are safe:
31
+ * those removals run inside the parked subtree, where every parent-child
32
+ * link React recorded still holds. Parking is also fully reversible — any
33
+ * decline path (desktop, nothing worth moving, teardown) hands every node
34
+ * back before anything is removed.
31
35
  */
32
36
  export declare function installModelSheetExtras(ctx: ClientContext): void;
33
37
  //# sourceMappingURL=model-sheet-extras.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-sheet-extras.d.ts","sourceRoot":"","sources":["../../../../src/client/effects/model-sheet-extras.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAoEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAkEhE"}
1
+ {"version":3,"file":"model-sheet-extras.d.ts","sourceRoot":"","sources":["../../../../src/client/effects/model-sheet-extras.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AA6FvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CA+JhE"}
@@ -1,2 +1,2 @@
1
- export declare const COMPOSER_CSS = "/* ---------- phone composer (< 768px) ---------- */\n\n/* The file picker the attachment button drives (S7) \u2014 hidden at EVERY width,\n deliberately OUTSIDE the phone media block below. A bare <input type=\"file\">\n renders as a native \"Choose Files\" control, and the slot wrapper around it is\n `display: contents`, so an un-hidden one becomes a flex item of the official\n tool row. Scoping this to < 768px once put that control in the DESKTOP\n composer (caught on a live deploy, 2026-08-17): the button that drives it is\n phone-only, but the input it drives is in the DOM at all widths. */\n[data-mobile-nav=\"attach-picker\"] {\n display: none !important;\n}\n\n@media (max-width: 767px) {\n /* --- 1. flatten the two official groups ---\n `_tools` and `_trailing` only exist to cluster controls left/right.\n display:contents dissolves them into `_row`, so the six leaf controls\n become siblings in one flex line and `order` alone drives the layout:\n [attach \u00B7 + \u00B7 permission \u00B7 model] \u2026elastic gap\u2026 [context ring \u00B7 send]. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] {\n justify-content: flex-start !important;\n gap: 5px !important;\n padding: 2px 8px 8px !important;\n /* One line, always: the model pill is the only shrinkable item (its\n label ellipsizes), so wrapping would only ever push the send button\n to a second row (real-device report, 2026-08-25). */\n flex-wrap: nowrap !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] {\n display: contents !important;\n }\n\n /* --- 2. the running order ---\n The attachment button (S3 placeholder, S7 wires it up) sits leftmost;\n the official \"+\" command menu keeps its seat right next to it, where\n the two \"insert something\" affordances read as one cluster. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] [data-mobile-nav=\"attach\"] {\n order: 1 !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_add\"] {\n order: 2 !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] {\n order: 3 !important;\n /* Rigid on purpose. This seat holds the permission chip, whose own\n wrapper (the plugin's element inside _modes) is `flex: 0 0 auto` and\n will NOT follow the seat down \u2014 so a shrinkable seat does not make the\n chip narrower, it just slices the seat out from under a chip that\n stays 44px wide, and the overflow lands on top of the model pill.\n Measured 2026-09-04 on DSH 0.1.2 at 390px: seat 90-120 (29.7px) with\n the chip still 90-134, model starting at 125 \u2014 a 9px overlap, exactly\n the \"the two chips touch\" report. With the seat rigid: seat 90-134,\n model 139-246, overlap 0; and at 320px the model pill shrinks to 52px\n and still nothing overlaps.\n This also restores what section 1 says out loud \u2014 the model pill is\n the ONLY shrinkable item in the row. `min-width: 0` is gone with the\n shrink: it only ever mattered as the companion of `flex: 0 1`. */\n flex: 0 0 auto !important;\n gap: 6px !important;\n }\n /* The elastic gap used to live here as `margin-right: auto`, which broke\n the moment a session had no model pill: a subagent session has none, so\n the gap vanished and the stop / send buttons bunched up in the middle of\n the row instead of sitting at the right edge (reported 2026-09-06). The\n gap belongs to the RIGHT group, not to the last item of the left one \u2014\n see the two rules below the order list. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] {\n order: 4 !important;\n /* A definite flex-basis (not auto) breaks the circular sizing between\n this seat and the trigger's max-width:100%: the seat's width comes\n from flex resolution, the trigger fills up to it, the label\n ellipsizes inside. Under nowrap pressure this is the only\n min-width:0 seat, so it absorbs all shrink. */\n flex: 0 1 min(48vw, 200px) !important;\n min-width: 0 !important;\n max-width: min(48vw, 200px) !important;\n }\n /* Third-party input.right entries park next to the ring rather than\n landing at order 0 (= far left) once the groups are flattened. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.right\"] > * {\n order: 5 !important;\n }\n /* ContextMeter. 0.1.7 moved it out of the row into ROOT's `_dock`, a\n line UNDER the card \u2014 on the phone that dropped the ring to the very\n bottom of the screen (user report, 2026-09-28). Lifted back beside send\n without moving any official node: ROOT becomes a one-cell grid and the\n dock shares the card's cell, pinned to the card's bottom-right corner,\n over a placeholder the row reserves in front of send (row::before at\n order 6, below). Anchoring to the card \u2014 not to ROOT's bottom edge \u2014\n matters: ROOT's bottom padding follows the safe-area inset, so an\n absolute `bottom` offset measured in the emulator sat low on a real\n iPhone (user screenshot, 2026-09-29). Vertical: the row's 8px bottom\n pad, 34px tall = the send button's box. Horizontal: row pad 8 + send 34\n + gap 5. The percentage is dropped on the phone (user call): ring only. */\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"]:has(> [class$=\"_dock\"] button) {\n display: grid !important;\n grid-template-columns: minmax(0, 1fr) !important;\n justify-items: center !important;\n }\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"]:has(> [class$=\"_dock\"] button) > [class$=\"_card\"] {\n grid-area: 1 / 1 !important;\n }\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"] > [class$=\"_dock\"] {\n grid-area: 1 / 1 !important;\n align-self: end !important;\n justify-self: end !important;\n position: relative !important;\n z-index: 1;\n height: 34px !important;\n margin: 0 47px 8px 0 !important;\n padding: 0 !important;\n gap: 0 !important;\n }\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"] > [class$=\"_dock\"] button[class$=\"_trigger\"] {\n padding: 8px !important;\n gap: 0 !important;\n }\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"] > [class$=\"_dock\"] button[class$=\"_trigger\"] > span {\n display: none !important;\n }\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"]:has(> [class$=\"_dock\"] button) > [class$=\"_card\"] > [class$=\"_row\"]::before {\n content: '';\n order: 6;\n flex: 0 0 30px;\n margin-left: auto;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_primary\"] {\n order: 7 !important;\n }\n /* The elastic gap. It sits in front of whichever of the two right-hand\n items comes first, so the group hugs the right edge no matter what the\n left side happens to contain \u2014 with a model pill, without one (subagent\n sessions), with or without the third-party entries at order 5.\n Send carries `margin-left: auto` on its own; when the lifted ring's\n placeholder (row::before, above) precedes it, the placeholder carries\n the auto margin instead and the rule below takes it back off send \u2014\n two auto margins would SHARE the free space and open a second gap\n between the ring and send. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_primary\"] {\n margin-left: auto !important;\n }\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"]:has(> [class$=\"_dock\"] button) > [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_primary\"] {\n margin-left: 0 !important;\n }\n\n /* --- 3. permission as icon-only pill; model as name+effort pill ---\n (real-device round 2, 2026-08-17; model pill reworked 2026-08-25:\n the user wants to SEE which model/effort is active, so only the\n permission trigger stays icon-only \u2014 its label is hidden and it\n collapses to a plain ~44x30 icon button. This AGREES with (rather than fights) the official\n container query (`@container (width <= 460px) { .trigger:has(.triggerIcon)\n .triggerLabel { display: none } }`) that S3 had to override \u2014 no need\n to override it back. Accessible name is unaffected: both official\n triggers already ship a descriptive aria-label independent of the\n visible text (PermissionSelect: t('input.accessMode', {name}); the\n model trigger: t('trigger.aria'/'trigger.ariaEffort')) \u2014 verified live\n in dsh-client-ui-conversation / dsh-client-ui-model-selection lib/\n client.js, 2026-08-17 \u2014 so hiding the label costs nothing for screen\n readers. The permission trigger only renders an icon for the\n \"read-only\" / \"workspace-write\" presets (permissionGlyphs in\n dsh-client-ui-conversation) \u2014 \"Full access\" and any host-configured\n preset name fall back to chevron-only, a known gap in the official\n markup this plugin cannot fill without inventing new icon meaning.\n The model trigger never renders an icon at all (only label + optional\n effort text + chevron), so its ::before below draws one from a\n primitives icon path (Sparkle \u2014 the closest existing \"model\" glyph) as\n a CSS-only pseudo-element: it survives React re-renders for free\n (unlike a DOM-injected node, which would need a MutationObserver, see\n the preview-full-toggle pitfall in AGENTS.md) and does not touch\n accessible-name computation (empty generated content). S3's\n rtl-ellipsis trick on the model label is simply inert under\n display:none now; left alone rather than unpicked. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [class$=\"_triggerLabel\"] {\n display: none !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] button[class$=\"_trigger\"] {\n background: var(--dsw-specific-selector, rgba(127, 127, 127, .12)) !important;\n width: 44px !important;\n height: 30px !important;\n max-width: none !important;\n min-width: 0 !important;\n padding: 0 !important;\n gap: 2px !important;\n justify-content: center !important;\n border-radius: 999px !important;\n touch-action: manipulation !important;\n }\n /* Model trigger: name + effort pill (2026-08-25 user request \u2014 the\n icon-only form hid which model was active). The effort text must stay\n whole; the model NAME is the elastic part and ellipsizes. max-width\n caps the pill so the row's elastic gap survives extreme model ids;\n the label rules below override the <=1023px grow rules in\n layout.css.ts (flex 1 1 auto) that would otherwise pad the pill. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_trigger\"] {\n background: var(--dsw-specific-selector, rgba(127, 127, 127, .12)) !important;\n height: 30px !important;\n width: auto !important;\n max-width: 100% !important;\n min-width: 0 !important;\n padding: 0 10px !important;\n gap: 4px !important;\n justify-content: flex-start !important;\n border-radius: 999px !important;\n touch-action: manipulation !important;\n font-size: 12px !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_trigger\"] > [class$=\"_triggerLabel\"] {\n display: block !important;\n flex: 0 1 auto !important;\n min-width: 0 !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n white-space: nowrap !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_trigger\"] > [class$=\"_triggerEffort\"] {\n display: block !important;\n flex: none !important;\n white-space: nowrap !important;\n }\n /* PermissionSelect wraps its trigger in the Menu primitive's root span,\n which must shrink to the icon button's fixed width. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] > span:has(> button[class$=\"_trigger\"]) {\n flex: 0 0 auto !important;\n min-width: 0 !important;\n }\n /* (2026-08-25) The model trigger's CSS-only Sparkle ::before icon is gone:\n the pill now shows the model name + effort text itself, so an icon would\n just eat label width. */\n\n /* --- 4. both menus become bottom sheets ---\n The permission menu is the Menu primitive (role=menu, absolute, side=top)\n and the model menu is ModelSelect's own `_menu` (absolute, bottom+right\n under the pill on \u2264 0.1.2; a body portal on 0.1.5+ \u2014 see MODEL_MENU).\n Neither has a transformed ancestor between it and the viewport, so\n position:fixed re-anchors both to the screen edge. Only the shell moves \u2014\n the items, the two-level model panes and every selection handler stay\n official. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [role=\"menu\"],\n /* T70: the SAME permission menu, portaled to body on DSH 0.2.0 \u2014 the\n descendant selector above cannot reach it there. The marker is written\n by effects/permission-sheet.ts: the trigger's click arms a short window\n and the first body-level menu to land inside it is ours \u2014 the host has\n many other portal Menus (preset pickers, preference rows, the sidebar's\n \u2026, open-in-app, deliverables, \u2026), and none opens through this trigger\n while no marked menu is up. */\n body > div[role=\"menu\"][data-zen-sheet=\"perm\"],\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) {\n position: fixed !important;\n left: 0 !important;\n right: 0 !important;\n bottom: 0 !important;\n top: auto !important;\n transform: none !important;\n width: 100% !important;\n min-width: 0 !important;\n max-width: none !important;\n max-height: min(70dvh, 520px) !important;\n box-sizing: border-box !important;\n border-radius: 16px 16px 0 0 !important;\n border-bottom: none !important;\n padding: 8px 8px calc(8px + var(--mnav-sab)) !important;\n /* 60 for the body-portaled menu too: the host gives it 1100 for desktop\n stacking, but nothing of ours or the host's sits between 60 and the\n sheet on a phone (hit-tested at four points on 0.1.5-rc.2, 2026-09-21),\n and 60 keeps it under the session-info sheet's 70 like the PERM one. */\n z-index: 60 !important;\n box-shadow: 0 -8px 32px rgba(0, 0, 0, .18) !important;\n }\n /* 44pt+ rows in both sheets (Menu items, model options, and the model\n sheet's two root cells that drill into the model / effort panes).\n The model panes are matched by ROLE too: on DSH 0.2.0 the selected\n option's class list ends in `_selected` (`\u2026_option \u2026_selected`), so the\n class-suffix selector missed exactly the checked row \u2014 it fell back to\n the host's 34px pill while its siblings got 48px. The root cells are\n role=menuitem, the options role=menuitemradio; the class forms stay for\n older hosts. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [role=\"menu\"] [role=\"menuitem\"],\n body > div[role=\"menu\"][data-zen-sheet=\"perm\"] [role=\"menuitem\"],\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) [role=\"menuitem\"],\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) [role=\"menuitemradio\"],\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) [class$=\"_option\"],\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) [class$=\"_cell\"] {\n min-height: 48px !important;\n border-radius: 12px !important;\n font-size: 15px !important;\n }\n /* --- 4a. third-party composer entries live in the model sheet ---\n effects/model-sheet-extras.ts parks the `conversation.input.right`\n container inside the model menu while that menu is open and puts it back\n when it closes; these rules are the two halves of that trip.\n\n In the row: hidden, but ONLY once the effect has marked the container as\n one it manages. Keying the hide off the marker rather than off \"not\n currently parked\" is what makes the failure direction safe \u2014 if the\n effect declines to act (nothing in the slot but the vision toggle, so\n nothing worth moving) or never runs at all, the marker is absent, this\n rule does not match, and the controls stay exactly where the host put\n them. The inverse spelling hid them in the row whenever they were not in\n the sheet, which in those same cases left them reachable from neither\n place (caught in review, 2026-09-06).\n Hiding by ancestor rather than by a display rule on the container itself\n is what lets the parked copy style itself freely \u2014 same element, two\n homes, two rule sets. `!important` because the slot container carries an\n inline `display: contents`. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.right\"][data-zen-sheet-extras] {\n display: none !important;\n }\n /* Parked in the sheet: a column of full-width rows in the sheet's own\n language, so a relocated control reads as a sibling of \u6A21\u578B and \u63A8\u7406\u7B49\u7EA7\n rather than a chip that wandered in. */\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) > [data-zen-sheet-extras] {\n display: flex !important;\n flex-direction: column !important;\n gap: 2px !important;\n width: 100% !important;\n }\n /* Undo the row's compaction on every parked control: back to full width and\n the 48px the sheet's own cells use. `> * > *` reaches both shapes without\n naming either plugin \u2014 subscriptions wraps its trigger in a positioning\n div, vision-router puts its button straight in the slot. */\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) > [data-zen-sheet-extras] > *,\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) > [data-zen-sheet-extras] > * > button {\n width: 100% !important;\n max-width: none !important;\n min-width: 0 !important;\n justify-content: flex-start !important;\n }\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) > [data-zen-sheet-extras] button {\n box-sizing: border-box !important;\n min-height: 48px !important;\n height: auto !important;\n padding: 0 12px !important;\n gap: 10px !important;\n border-radius: 12px !important;\n border: none !important;\n background: transparent !important;\n font-size: 15px !important;\n /* Both plugins center their chip label, which is right for a pill in the\n row and wrong for a full-width row: the sheet's own cells start their\n label at the left edge. justify-content covers the flex triggers,\n text-align the ones that are not flex containers. */\n text-align: left !important;\n justify-content: flex-start !important;\n }\n /* Order comes from the row (compat.css.ts puts the vision toggle at 5 so it\n parks beside the context ring), and that rule still matches in here on\n \u2264 0.1.2 \u2014 the inline sheet is a DOM descendant of the composer bar even\n though it paints as a fixed layer (the 0.1.5 body portal is not, where\n this is a harmless no-op). Reset it so the sheet follows DOM order\n instead of inheriting a decision that was about a different layout. */\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) > [data-zen-sheet-extras] > * {\n order: 0 !important;\n }\n /* The vision toggle is squeezed to a 28px icon in the row (compat.css.ts);\n in the sheet it is a row, so its label comes back and the icon leads. */\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) > [data-zen-sheet-extras] [data-vision-router-mode-toggle] > span {\n display: inline !important;\n }\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) > [data-zen-sheet-extras] [data-vision-router-mode-toggle] > svg:first-of-type {\n width: 18px !important;\n height: 18px !important;\n }\n /* The speed chip opens a menu of its own, anchored `bottom: 100%` to its\n trigger. Inside the sheet \u2014 which is `overflow: hidden` so its rounded\n top clips its content \u2014 that menu would be cut off. Same escape section 4\n uses for the host's two menus: position: fixed leaves the clip behind\n (the sheet sets `transform: none`, so nothing between here and the\n viewport captures fixed positioning) and it lands as its own sheet on top\n of this one. */\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) > [data-zen-sheet-extras] [role=\"menu\"] {\n position: fixed !important;\n left: 0 !important;\n right: 0 !important;\n bottom: 0 !important;\n top: auto !important;\n margin: 0 !important;\n width: 100% !important;\n min-width: 0 !important;\n max-width: none !important;\n max-height: min(70dvh, 520px) !important;\n box-sizing: border-box !important;\n border-radius: 16px 16px 0 0 !important;\n border-bottom: none !important;\n padding: 8px 8px calc(8px + var(--mnav-sab)) !important;\n /* Above the model sheet it sits on (60, section 4), below the session-info\n sheet's 70 (info.css.ts) \u2014 it is a child of the model sheet, not a peer\n of the page's own layers. */\n z-index: 61 !important;\n box-shadow: 0 -8px 32px rgba(0, 0, 0, .18) !important;\n }\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) > [data-zen-sheet-extras] [role=\"menu\"] [role=\"menuitemradio\"] {\n min-height: 48px !important;\n border-radius: 12px !important;\n font-size: 15px !important;\n }\n\n /* --- 4b. the official scroll-to-bottom button must not poke through the\n sheet (real-device follow-up, 2026-08-17) ---\n ChatView's own \"jump to latest\" button (aria-label t(\"chat.toBottom\"))\n is `position: sticky; z-index: 8` inside the message column, not\n `position: fixed` \u2014 it never escapes to the same top-level stacking\n context our sheets get promoted to, so raising the sheet's z-index\n further does nothing (measured live: it still rendered on top at\n z-index 60 vs 8). Rather than chase engine-specific stacking-context\n semantics (Chromium and WebKit do not always agree here \u2014 see AGENTS.md\n CDP/document.hidden lesson for another instance of that), this hides\n the button outright while either sheet is open and lets it reappear on\n close: a plain `display: none` behind a live `:has()` read is correct\n regardless of which stacking rules the engine happens to apply.\n Selector: no hashed classes (dsh-client-ui-conversation lib/client.js,\n verified 2026-08-17) \u2014 `data-chat-flow` is the one stable attribute on\n the message column, and the button's wrapper is its only sibling\n (ChatView only ever renders the column and, conditionally, this one\n slot), so the adjacent-sibling combinator pins it precisely. */\n body:has([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [role=\"menu\"]) [data-chat-flow] + div,\n body:has([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"]) [data-chat-flow] + div,\n body:has(> [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) [data-chat-flow] + div,\n /* T70: the portaled permission menu (marked by effects/permission-sheet.ts). */\n body:has(> div[role=\"menu\"][data-zen-sheet=\"perm\"]) [data-chat-flow] + div {\n display: none !important;\n }\n /* --- 5. input box: two lines minimum, five lines maximum ---\n The official autosizer drives the box off the hidden mirror's height, so\n a min-height on the mirror IS the min-height of the field; the scroll cap\n rides the official custom property. 52px = 2 x 24px line box + 4px pad,\n 124px = 5 lines. The hero card keeps its own one-line collapse (misc.css\n pins _scroll/_grow while the placeholder shows). */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] [class$=\"_mirror\"] {\n min-height: 52px !important;\n }\n [class$=\"_composerSeat\"] {\n --dsh-composer-text-max-height: 124px;\n }\n /* Pull the text in from the card's left border. Upstream insets the editing\n host by 16px while the control row below it starts 8px in (row padding,\n section 1), so the placeholder sat noticeably further out than the\n buttons under it and the field read as over-padded on a phone (reported\n 2026-09-06). 12px squares it with the 12px of clear space on the right,\n giving the text a symmetric box, and closes most of the gap with the\n button row. Anchored on the editing host's own contract attribute rather\n than a hashed class. */\n [data-slot=\"conversation.composer.bar\"] [data-composer-input] {\n padding-left: 12px !important;\n }\n\n /* --- 6. no divider above OR below the message list ---\n Instead of a rule the messages butt against, they fade near both edges.\n This used to be ONE mask-image on the message scroller; iOS WebKit\n turned that into fog over the header (user report 2026-08-23): a mask\n on a scroll container is applied in CONTENT coordinates there (the fade\n bands scroll away with the messages) and, worse, it breaks the\n container's own overflow clipping \u2014 content scrolled out of view keeps\n painting, straight over the header and status bar. Engine bug with no\n iOS escape hatch (every iOS browser is WebKit), so the fades moved off\n the scroller entirely:\n\n - fade-IN under the header: the header's own ::after, restyled into a\n 20px gradient strip \u2014 styles/header.css.ts (\"no header bottom line\").\n - fade-OUT above the composer: nothing of ours. The official composer\n seat already carries `background: linear-gradient(transparent 0,\n bg 36px)` on its sticky self (dsh-client-ui-conversation lib/\n client.js .wSkVaW_composerSeat) and that is the entire visible\n effect; the mask's own bottom band landed inside the safe-area\n padding below the card and painted nothing. If the bottom edge ever\n reads harsh, hang a ::before strip off the seat the same way the\n header does \u2014 do NOT put a mask back on the scroller.\n\n Painted strips instead of a mask is a fair trade only because the page\n background is one flat color \u2014 which it is (--dsw-alias-bg-base). */\n [class$=\"_composerSeat\"],\n [class$=\"_composerStack\"] {\n border-top: none !important;\n }\n\n /* --- 7. dock entries become mini chips above the input card ---\n conversation.input.dock is display:contents (inline style), so its\n entries are stacked rows of the composer column. Forcing the slot itself\n to flex turns them into ONE scrollable chip line outside and above the\n card. Written against the slot, not against any particular plugin's chip\n (the git branch chip and the todo panel are third-party and may not be\n installed at all). */\n [data-slot=\"conversation.input.dock\"] {\n display: flex !important;\n flex-flow: row nowrap !important;\n align-items: center !important;\n gap: 6px !important;\n min-width: 0 !important;\n margin: 0 16px !important;\n overflow-x: auto !important;\n overflow-y: hidden !important;\n scrollbar-width: none !important;\n /* The dock is itself a flex ITEM of the composer stack, and the\n new-session hero pins that stack to a fixed height. overflow-x:auto\n above makes this a scroll container, which drops a flex item's\n automatic minimum size to 0 \u2014 so the hero squeezed the dock to ~5px\n and the attachment chips slid under the card drawn after it (only the\n top sliver of a thumbnail showed; reported from a real phone,\n 2026-08-26). Refuse the squeeze: natural height, the stack centers a\n few px lower instead. In-chat the column is not height-pinned and this\n is a no-op. */\n flex-shrink: 0 !important;\n }\n [data-slot=\"conversation.input.dock\"]::-webkit-scrollbar {\n display: none !important;\n }\n [data-slot=\"conversation.input.dock\"] > * {\n flex: 0 0 auto !important;\n max-width: 70% !important;\n min-height: 26px !important;\n max-height: 26px !important;\n border-radius: 999px !important;\n font-size: 11.5px !important;\n line-height: 18px !important;\n overflow: hidden !important;\n }\n /* The git-graph chip carries a 34px tablet target from misc.css; that\n selector is more specific, so restate it at the phone breakpoint. */\n [data-slot=\"conversation.input.dock\"] [data-gitgraph-chip-anchor] [data-gitgraph-chip] {\n min-height: 26px !important;\n padding: 0 10px !important;\n font-size: 11.5px !important;\n }\n /* --- 7a. the native to-do card opts OUT of the 26px pill cage ---\n conversation.input.dock is not only a chip rail: DSH's own TodoPanel\n (dsh-client-ui-conversation) registers here, and it is an expandable\n card, not a pill \u2014 tapping its header renders the to-do list INSIDE\n itself. The cage's max-height:26px + overflow:hidden left that list\n rendered but clipped to nothing, so on a phone the bar looked dead:\n aria-expanded flipped, 8 rows mounted at 216px, and the card still\n painted 26px tall (measured live, 2026-08-20). Give it the official\n geometry back \u2014 full row, natural height, 12px radius. Its list caps\n itself at 180px with its own scroller, so this cannot run into the\n composer. Anchored on the official data-testid, a stable contract\n marker rather than a hashed class name. */\n [data-slot=\"conversation.input.dock\"] > [data-testid=\"todo-panel\"] {\n /* A whole line to itself. It shared line 0 with the pills until the goal\n bar turned up (2026-09-06): two full-width things on one nowrap line\n meant the to-do header shrank to a stub and the goal bar ran off the\n right edge. Both are status strips you read at a glance, so they stack\n \u2014 see the line policy in 7c. */\n flex: 1 0 100% !important;\n order: 1 !important;\n min-width: 0 !important;\n max-width: none !important;\n min-height: 0 !important;\n max-height: none !important;\n overflow: visible !important;\n border-radius: 12px !important;\n /* The cage shrinks dock text to 11.5px/18px; the card's rows inherit it\n and turn cramped. Restore the official 13px body scale. */\n font-size: 13px !important;\n line-height: normal !important;\n }\n\n /* --- 7b. attachment chips (S7.1) ---\n Our own dock entry opts OUT of the 26px pill cage above: an image chip is\n a 48px tile. The row sits tight under the card's top edge, so the chips\n read as part of the composer rather than as a floating strip.\n\n It also takes the LAST line of its own (line policy in 7c): a 48px tile\n beside the to-do chip on one nowrap line squeezed that chip down to an\n unreadable stub, with a long file name pushing the rest off-screen\n (reported 2026-08-22). Its own chips wrap rather than overflow, so\n several attachments stack instead of scrolling out of reach. */\n [data-slot=\"conversation.input.dock\"] > [data-mobile-nav=\"attach-chips\"] {\n display: flex !important;\n flex: 1 0 100% !important;\n flex-wrap: wrap !important;\n order: 4 !important;\n align-items: center !important;\n gap: 6px !important;\n max-width: none !important;\n min-height: 0 !important;\n max-height: none !important;\n border-radius: 0 !important;\n overflow: visible !important;\n margin-bottom: -2px;\n }\n\n /* --- 7b2. the T34 remote-status banner opts OUT of the cage ---\n Same class of entry as the to-do card (7a): a full-width status strip,\n not a pill. dsh-zen-remote's remote banner registers into this dock at\n order 15 when a remote session is offline or closed (T34) \u2014 the 26px\n cage would clip the copy to one unreadable sliver. A whole line of its\n own, natural height. */\n [data-slot=\"conversation.input.dock\"] > [data-zen-remote=\"remote-banner\"] {\n flex: 1 0 100% !important;\n order: 2 !important;\n max-width: none !important;\n min-height: 0 !important;\n max-height: none !important;\n overflow: visible !important;\n border-radius: 12px !important;\n font-size: 12px !important;\n line-height: 18px !important;\n }\n\n /* --- 7c. the queue strip opts OUT of the cage, on a line of its own ---\n Third entry that is not a pill. conversation.input.dock is a `list` slot\n and DSH's own QueueDock registers into it at order 20 (id `queue`,\n dsh-client-ui-conversation) \u2014 a 36px row per queued message, or a\n collapsible \"N \u6761\u6392\u961F\u6D88\u606F\" header once there is more than one. The 26px\n cage above clipped it to a sliver: on a phone a queued message showed as\n a grey stub poking out from behind the input card, unreadable and with\n its edit / delete / send buttons cut off (reported 2026-08-22, same\n failure the to-do card hit in 7a).\n\n Two things are restored. Geometry: the official row height back, and the\n dock's own composer-column sizing dropped \u2014 its width/max-width are\n calc()s over the composer card variables and its bottom margin is\n negative by design (upstream tucks the strip UNDER the card), none of\n which survives being a flex item of our chip rail.\n\n Layout \u2014 the line policy for the whole rail, now that three things want\n room on it. The pills (the to-do card, a branch chip, whatever else\n registers) keep order 0 and share the top line, still scrolling\n horizontally. The queue takes the next line (order 1). The attachment\n chips take the last one (order 2, 7b), so what the user is about to send\n sits closest to the input it will be sent from. The rail only wraps\n while one of the two full-width entries exists, so a session with\n neither keeps the original single scrolling chip line. */\n [data-slot=\"conversation.input.dock\"]:has(> [data-queue-dock], > [data-mobile-nav=\"attach-chips\"], > [data-testid=\"todo-panel\"], > [data-goal-bar]) {\n flex-wrap: wrap !important;\n }\n [data-slot=\"conversation.input.dock\"] > [data-queue-dock] {\n flex: 1 0 100% !important;\n order: 3 !important;\n max-width: none !important;\n min-height: 0 !important;\n max-height: none !important;\n margin: 0 !important;\n padding: 0 !important;\n border-radius: 0 !important;\n overflow: visible !important;\n font-size: 13px !important;\n line-height: normal !important;\n }\n /* dsh-client-ui-goal's status strip (its own `data-goal-bar` marker; it\n registers into this dock at order 10). Fourth thing here that is not a\n pill: a row carrying the objective text plus pause / edit / delete\n buttons, so the 26px cage clipped its buttons off the right edge and the\n nowrap line pushed what was left off screen (reported 2026-09-06). Same\n treatment the queue gets \u2014 a line of its own, official geometry back. */\n [data-slot=\"conversation.input.dock\"] > [data-goal-bar] {\n flex: 1 0 100% !important;\n order: 2 !important;\n max-width: none !important;\n min-height: 0 !important;\n max-height: none !important;\n border-radius: 12px !important;\n overflow: visible !important;\n font-size: 13px !important;\n line-height: normal !important;\n }\n\n /* The panel is the dock's only child; upstream rounds its top corners only\n and drops the bottom border, because there it is half-hidden behind the\n card. On its own line it is a free-standing strip, so close it up. */\n [data-slot=\"conversation.input.dock\"] > [data-queue-dock] > * {\n border-radius: 12px !important;\n }\n [data-slot=\"conversation.input.dock\"] > [data-queue-dock] > *::after {\n border-bottom: 1px solid var(--dsw-alias-border-l1) !important;\n }\n\n [data-mobile-nav=\"attach-chip\"] {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n flex: 0 0 auto;\n max-width: 62vw;\n background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .12));\n color: var(--dsw-alias-label-primary, inherit);\n }\n [data-mobile-nav=\"attach-chip\"][data-kind=\"file\"] {\n height: 26px;\n padding: 0 4px 0 8px;\n border-radius: 999px;\n font-size: 11.5px;\n line-height: 18px;\n }\n /* An image chip is the thumbnail: no name line, the filename lives in the\n title attribute (and in the draft text right below). */\n [data-mobile-nav=\"attach-chip\"][data-kind=\"image\"] {\n width: 48px;\n height: 48px;\n padding: 0;\n border-radius: 10px;\n overflow: hidden;\n }\n [data-mobile-nav=\"attach-chip-art\"] {\n position: relative;\n display: grid;\n place-items: center;\n flex: none;\n overflow: hidden;\n }\n [data-kind=\"file\"] > [data-mobile-nav=\"attach-chip-art\"] {\n width: 16px;\n height: 16px;\n opacity: .7;\n }\n [data-kind=\"image\"] > [data-mobile-nav=\"attach-chip-art\"] {\n width: 100%;\n height: 100%;\n }\n /* The <img> is stacked over the paperclip, not swapped for it: a format the\n engine cannot decode (HEIC outside WebKit) simply paints nothing and the\n icon shows through \u2014 no onError state to carry. */\n [data-mobile-nav=\"attach-chip-art\"] img {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n background: var(--dsw-alias-bg-layer-2, rgba(127, 127, 127, .18));\n }\n [data-mobile-nav=\"attach-chip-name\"] {\n min-width: 0;\n overflow: hidden;\n white-space: nowrap;\n }\n [data-mobile-nav=\"attach-chip-remove\"] {\n display: grid;\n place-items: center;\n flex: none;\n width: 18px;\n height: 18px;\n padding: 0;\n border: none;\n border-radius: 999px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n touch-action: manipulation;\n }\n /* On a tile the \u00D7 floats in the corner over the picture, so it needs its own\n ground to stay legible against an arbitrary photo. */\n [data-kind=\"image\"] > [data-mobile-nav=\"attach-chip-remove\"] {\n position: absolute;\n top: 2px;\n right: 2px;\n background: rgba(0, 0, 0, .55);\n color: #fff;\n }\n [data-mobile-nav=\"attach-chip-remove\"]:active {\n transform: scale(.9);\n }\n\n /* --- 8. the official stats strip leaves the composer ---\n Its data moves into the session info card (S4). The row is the composer\n dock's own `_root` entry; the slot itself stays live for later entries. */\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"],\n [data-slot=\"conversation.composer.dock\"] > [data-composer-stat] {\n display: none !important;\n }\n\n /* --- 9. the attachment button (S7; its file input is hidden at the top of\n this file, at every width) --- */\n [data-mobile-nav=\"attach\"] {\n position: relative;\n width: 28px !important;\n height: 28px !important;\n flex: none !important;\n display: grid !important;\n place-items: center !important;\n padding: 0 !important;\n border: none !important;\n border-radius: 999px !important;\n background: var(--dsw-specific-selector, rgba(127, 127, 127, .12));\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n touch-action: manipulation;\n }\n [data-mobile-nav=\"attach\"]:active {\n transform: scale(.94);\n transition: transform .12s;\n }\n /* Busy: a ring sweeps around the paperclip. Drawn with a conic gradient in\n a pseudo-element rather than a spinner node, so the official React tree\n around us has nothing extra to re-render (same reasoning as the model\n pill's mask icon, S3.1). */\n [data-mobile-nav=\"attach\"][data-busy]::after {\n content: \"\";\n position: absolute;\n inset: -2px;\n border-radius: 999px;\n background: conic-gradient(from 0deg, transparent 0 65%, currentColor 100%);\n mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));\n -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));\n animation: mnav-attach-spin .9s linear infinite;\n pointer-events: none;\n }\n @keyframes mnav-attach-spin {\n to { transform: rotate(1turn); }\n }\n @media (prefers-reduced-motion: reduce) {\n [data-mobile-nav=\"attach\"][data-busy]::after {\n animation-duration: 3s;\n }\n }\n /* Failure note: one line above the button, tap-to-dismiss. Sits on the\n composer card (z 2) rather than in an overlay layer \u2014 nothing here needs\n to clear the permission/model sheets. */\n [data-mobile-nav=\"attach-error\"] {\n position: absolute;\n bottom: calc(100% + 6px);\n left: 0;\n z-index: 2;\n max-width: 62vw;\n padding: 5px 8px;\n border-radius: 8px;\n font-size: 12px;\n line-height: 1.3;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, .82));\n color: var(--dsw-alias-label-primary, #fff);\n box-shadow: 0 2px 8px rgba(0, 0, 0, .24);\n }\n\n /* --- 9. home-indicator clearance (S4.1, 2026-08-17) ---\n Owned HERE, not by the gateway half. That plugin used to carry\n [data-slot=\"conversation.composer\"] { padding-bottom: max(env(safe-area-inset-bottom), 8px) }\n in both pwa/app.css and the gateway's inline DEVICE_CSS. Both were\n INERT and had always been: that slot element is `display: contents`\n (measured 2026-08-17 \u2014 the slot wrapper generates no box, so padding on\n it is discarded), which is why raising it never moved anything. The rule\n is deleted on the PWA side and restated here on an element that actually\n lays out, reading --mnav-sab so ?mobile-nav-inset=54,34 can regress it\n off-device (env() is hard 0 on desktop \u2014 the whole reason S2.1 exists).\n\n Target: the card should look EQUALLY inset on all four sides. This same\n element carries the card's side inset (`padding: 0 16px` officially,\n measured 16px left and 16px right at 390px), so the bottom gap is simply\n capped at that same 16px. Clearing the FULL 34px inset \u2014 the naive\n max(sab, 8px) \u2014 is what read as \"too thick\": it is more than double the\n side margin, so the card looked shoved up off the bottom edge.\n\n clamp() says all three requirements at once:\n - floor 8px -> a device with no home indicator (sab: 0) keeps the\n official 8px exactly, so this is a strict no-op there;\n - track sab -> a shallower inset than 16px is honoured as-is;\n - cap 16px -> a full-size indicator (sab: 34) lands on 16px, equal\n to the side inset, which is the look being asked for.\n Keep the 16px in step with the side padding above if that ever changes;\n that equality IS the spec here, not a coincidence. */\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"] {\n padding-bottom: clamp(8px, var(--mnav-sab), 16px) !important;\n }\n\n /* S10 keyboard avoid: effects/keyboard-avoid.ts mirrors the band of the\n layout viewport hidden behind the software keyboard into --mnav-kb-lift\n and stamps the html attribute while (and only while) it is non-zero \u2014\n a resting transform, even translateY(0), would silently become the\n containing block for any fixed-position descendant of the bar. In every\n environment where the browser's own focus-reveal works the attribute is\n simply never set.\n The target is the slot's _root CHILD, not the slot wrapper: the wrapper\n is display:contents \u2014 it generates no box, so a transform on it is a\n silent no-op (found live on-device via CDP, 2026-08-21; the variable and\n attribute were set and nothing moved). */\n html[data-mnav-kb] [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"] {\n transform: translateY(calc(-1 * var(--mnav-kb-lift, 0px)));\n }\n}\n\n/* The attachment button and its preview row only exist for the phone shell.\n Both render at every width (the slots are not breakpoint-aware), so both\n need hiding here \u2014 the dock-row rules that shape the chips live in the\n < 768px block and would otherwise leave a bare, unstyled chip line in the\n desktop composer. */\n@media (min-width: 768px) {\n [data-mobile-nav=\"attach\"],\n [data-mobile-nav=\"attach-chips\"] {\n display: none !important;\n }\n}\n";
1
+ export declare const COMPOSER_CSS = "/* ---------- phone composer (< 768px) ---------- */\n\n/* The file picker the attachment button drives (S7) \u2014 hidden at EVERY width,\n deliberately OUTSIDE the phone media block below. A bare <input type=\"file\">\n renders as a native \"Choose Files\" control, and the slot wrapper around it is\n `display: contents`, so an un-hidden one becomes a flex item of the official\n tool row. Scoping this to < 768px once put that control in the DESKTOP\n composer (caught on a live deploy, 2026-08-17): the button that drives it is\n phone-only, but the input it drives is in the DOM at all widths. */\n[data-mobile-nav=\"attach-picker\"] {\n display: none !important;\n}\n/* The composer-chips strip (effects/model-sheet-extras.ts) \u2014 same\n every-width default as the picker above: the phone block re-shows it\n inside the dock rail; the desktop shell keeps none. */\n[data-mobile-nav=\"composer-chips\"] {\n display: none !important;\n}\n\n@media (max-width: 767px) {\n /* --- 1. flatten the two official groups ---\n `_tools` and `_trailing` only exist to cluster controls left/right.\n display:contents dissolves them into `_row`, so the six leaf controls\n become siblings in one flex line and `order` alone drives the layout:\n [attach \u00B7 + \u00B7 permission \u00B7 model] \u2026elastic gap\u2026 [context ring \u00B7 send]. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] {\n justify-content: flex-start !important;\n gap: 5px !important;\n padding: 2px 8px 8px !important;\n /* One line, always: the model pill is the only shrinkable item (its\n label ellipsizes), so wrapping would only ever push the send button\n to a second row (real-device report, 2026-08-25). */\n flex-wrap: nowrap !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] {\n display: contents !important;\n }\n\n /* --- 2. the running order ---\n The attachment button (S3 placeholder, S7 wires it up) sits leftmost and\n is a PERMANENT resident now (#31): on DSH 0.2.x the standalone paperclip\n moved into the \"+\" command menu while its hidden <input type=\"file\">\n stayed in _tools, so the old host-attach stand-down probe matched on\n every host and the phone composer was left with no attach affordance at\n all. The official \"+\" command menu keeps its seat right next to it, where\n the two \"insert something\" affordances read as one cluster. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] [data-mobile-nav=\"attach\"] {\n order: 1 !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_add\"] {\n order: 2 !important;\n }\n /* DSH \u2264 0.1.5 still renders that second _add button (the standalone\n paperclip) as a direct _tools child; hide it below 768px so the row\n keeps exactly one attach control \u2014 ours, which uploads into the session\n workspace the phone can actually reach. On 0.2.x the only button in\n _tools is the first of its type, so this never matches. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > button[class$=\"_add\"]:nth-of-type(2) {\n display: none !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] {\n order: 3 !important;\n /* Rigid on purpose. This seat holds the permission chip, whose own\n wrapper (the plugin's element inside _modes) is `flex: 0 0 auto` and\n will NOT follow the seat down \u2014 so a shrinkable seat does not make the\n chip narrower, it just slices the seat out from under a chip that\n stays 44px wide, and the overflow lands on top of the model pill.\n Measured 2026-09-04 on DSH 0.1.2 at 390px: seat 90-120 (29.7px) with\n the chip still 90-134, model starting at 125 \u2014 a 9px overlap, exactly\n the \"the two chips touch\" report. With the seat rigid: seat 90-134,\n model 139-246, overlap 0; and at 320px the model pill shrinks to 52px\n and still nothing overlaps.\n This also restores what section 1 says out loud \u2014 the model pill is\n the ONLY shrinkable item in the row. `min-width: 0` is gone with the\n shrink: it only ever mattered as the companion of `flex: 0 1`. */\n flex: 0 0 auto !important;\n gap: 6px !important;\n }\n /* The elastic gap used to live here as `margin-right: auto`, which broke\n the moment a session had no model pill: a subagent session has none, so\n the gap vanished and the stop / send buttons bunched up in the middle of\n the row instead of sitting at the right edge (reported 2026-09-06). The\n gap belongs to the RIGHT group, not to the last item of the left one \u2014\n see the two rules below the order list. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > *:not([data-mobile-nav]) {\n order: 4 !important;\n /* A definite flex-basis (not auto) breaks the circular sizing between\n this seat and the trigger's max-width:100%: the seat's width comes\n from flex resolution, the trigger fills up to it, the label\n ellipsizes inside. Under nowrap pressure this is the only\n min-width:0 seat, so it absorbs all shrink. */\n flex: 0 1 min(48vw, 200px) !important;\n min-width: 0 !important;\n max-width: min(48vw, 200px) !important;\n }\n /* Third-party input.right entries park next to the ring rather than\n landing at order 0 (= far left) once the groups are flattened. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.right\"] > * {\n order: 5 !important;\n }\n /* ContextMeter. 0.1.7 moved it out of the row into ROOT's `_dock`, a\n line UNDER the card \u2014 on the phone that dropped the ring to the very\n bottom of the screen (user report, 2026-09-28). Lifted back beside send\n without moving any official node: ROOT becomes a one-cell grid and the\n dock shares the card's cell, pinned to the card's bottom-right corner,\n over a placeholder the row reserves in front of send (row::before at\n order 6, below). Anchoring to the card \u2014 not to ROOT's bottom edge \u2014\n matters: ROOT's bottom padding follows the safe-area inset, so an\n absolute `bottom` offset measured in the emulator sat low on a real\n iPhone (user screenshot, 2026-09-29). Vertical: the row's 8px bottom\n pad, 34px tall = the send button's box. Horizontal: row pad 8 + send 34\n + gap 5. The percentage is dropped on the phone (user call): ring only. */\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"]:has(> [class$=\"_dock\"] button) {\n display: grid !important;\n grid-template-columns: minmax(0, 1fr) !important;\n justify-items: center !important;\n }\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"]:has(> [class$=\"_dock\"] button) > [class$=\"_card\"] {\n grid-area: 1 / 1 !important;\n }\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"] > [class$=\"_dock\"] {\n grid-area: 1 / 1 !important;\n align-self: end !important;\n justify-self: end !important;\n position: relative !important;\n z-index: 1;\n height: 34px !important;\n margin: 0 47px 8px 0 !important;\n padding: 0 !important;\n gap: 0 !important;\n }\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"] > [class$=\"_dock\"] button[class$=\"_trigger\"] {\n padding: 8px !important;\n gap: 0 !important;\n }\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"] > [class$=\"_dock\"] button[class$=\"_trigger\"] > span {\n display: none !important;\n }\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"]:has(> [class$=\"_dock\"] button) > [class$=\"_card\"] > [class$=\"_row\"]::before {\n content: '';\n order: 6;\n flex: 0 0 30px;\n margin-left: auto;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_primary\"] {\n order: 7 !important;\n }\n /* The elastic gap. It sits in front of whichever of the two right-hand\n items comes first, so the group hugs the right edge no matter what the\n left side happens to contain \u2014 with a model pill, without one (subagent\n sessions), with or without the third-party entries at order 5.\n Send carries `margin-left: auto` on its own; when the lifted ring's\n placeholder (row::before, above) precedes it, the placeholder carries\n the auto margin instead and the rule below takes it back off send \u2014\n two auto margins would SHARE the free space and open a second gap\n between the ring and send. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_primary\"] {\n margin-left: auto !important;\n }\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"]:has(> [class$=\"_dock\"] button) > [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_primary\"] {\n margin-left: 0 !important;\n }\n\n /* --- 3. permission as icon-only pill; model as name+effort pill ---\n (real-device round 2, 2026-08-17; model pill reworked 2026-08-25:\n the user wants to SEE which model/effort is active, so only the\n permission trigger stays icon-only \u2014 its label is hidden and it\n collapses to a plain ~44x30 icon button. This AGREES with (rather than fights) the official\n container query (`@container (width <= 460px) { .trigger:has(.triggerIcon)\n .triggerLabel { display: none } }`) that S3 had to override \u2014 no need\n to override it back. Accessible name is unaffected: both official\n triggers already ship a descriptive aria-label independent of the\n visible text (PermissionSelect: t('input.accessMode', {name}); the\n model trigger: t('trigger.aria'/'trigger.ariaEffort')) \u2014 verified live\n in dsh-client-ui-conversation / dsh-client-ui-model-selection lib/\n client.js, 2026-08-17 \u2014 so hiding the label costs nothing for screen\n readers. The permission trigger only renders an icon for the\n \"read-only\" / \"workspace-write\" presets (permissionGlyphs in\n dsh-client-ui-conversation) \u2014 \"Full access\" and any host-configured\n preset name fall back to chevron-only, a known gap in the official\n markup this plugin cannot fill without inventing new icon meaning.\n The model trigger never renders an icon at all (only label + optional\n effort text + chevron), so its ::before below draws one from a\n primitives icon path (Sparkle \u2014 the closest existing \"model\" glyph) as\n a CSS-only pseudo-element: it survives React re-renders for free\n (unlike a DOM-injected node, which would need a MutationObserver, see\n the preview-full-toggle pitfall in AGENTS.md) and does not touch\n accessible-name computation (empty generated content). S3's\n rtl-ellipsis trick on the model label is simply inert under\n display:none now; left alone rather than unpicked. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [class$=\"_triggerLabel\"] {\n display: none !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] button[class$=\"_trigger\"] {\n background: var(--dsw-specific-selector, rgba(127, 127, 127, .12)) !important;\n width: 44px !important;\n height: 30px !important;\n max-width: none !important;\n min-width: 0 !important;\n padding: 0 !important;\n gap: 2px !important;\n justify-content: center !important;\n border-radius: 999px !important;\n touch-action: manipulation !important;\n }\n /* Model trigger: name + effort pill (2026-08-25 user request \u2014 the\n icon-only form hid which model was active). The effort text must stay\n whole; the model NAME is the elastic part and ellipsizes. max-width\n caps the pill so the row's elastic gap survives extreme model ids;\n the label rules below override the <=1023px grow rules in\n layout.css.ts (flex 1 1 auto) that would otherwise pad the pill. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > *:not([data-mobile-nav]) > [class$=\"_trigger\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > *:not([data-mobile-nav]) > button {\n background: var(--dsw-specific-selector, rgba(127, 127, 127, .12)) !important;\n height: 30px !important;\n width: auto !important;\n max-width: 100% !important;\n min-width: 0 !important;\n padding: 0 10px !important;\n gap: 4px !important;\n justify-content: flex-start !important;\n border-radius: 999px !important;\n touch-action: manipulation !important;\n font-size: 12px !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > *:not([data-mobile-nav]) > [class$=\"_trigger\"] > [class$=\"_triggerLabel\"] {\n display: block !important;\n flex: 0 1 auto !important;\n min-width: 0 !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n white-space: nowrap !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > *:not([data-mobile-nav]) > [class$=\"_trigger\"] > [class$=\"_triggerEffort\"] {\n display: block !important;\n flex: none !important;\n white-space: nowrap !important;\n }\n /* Third-party model selectors (\u2026codex-ui) render a bare <button> with no\n _trigger classes and text straight inside it; give that button the same\n shrink+truncate contract the official label gets, so a long model id\n ellipsizes instead of stretching the row. The official trigger also\n matches this (harmless: it already ellipsizes its own label). */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > *:not([data-mobile-nav]) > button {\n max-width: 100% !important;\n min-width: 0 !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n white-space: nowrap !important;\n }\n /* PermissionSelect wraps its trigger in the Menu primitive's root span,\n which must shrink to the icon button's fixed width. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] > span:has(> button[class$=\"_trigger\"]) {\n flex: 0 0 auto !important;\n min-width: 0 !important;\n }\n /* (2026-08-25) The model trigger's CSS-only Sparkle ::before icon is gone:\n the pill now shows the model name + effort text itself, so an icon would\n just eat label width. */\n\n /* --- 4. both menus become bottom sheets ---\n The permission menu is the Menu primitive (role=menu, absolute, side=top)\n and the model menu is ModelSelect's own `_menu` (absolute, bottom+right\n under the pill on \u2264 0.1.2; a body portal on 0.1.5+ \u2014 see MODEL_MENU).\n Neither has a transformed ancestor between it and the viewport, so\n position:fixed re-anchors both to the screen edge. Only the shell moves \u2014\n the items, the two-level model panes and every selection handler stay\n official. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [role=\"menu\"],\n /* T70: the SAME permission menu, portaled to body on DSH 0.2.0 \u2014 the\n descendant selector above cannot reach it there. The marker is written\n by effects/permission-sheet.ts: the trigger's click arms a short window\n and the first body-level menu to land inside it is ours \u2014 the host has\n many other portal Menus (preset pickers, preference rows, the sidebar's\n \u2026, open-in-app, deliverables, \u2026), and none opens through this trigger\n while no marked menu is up. */\n body > div[role=\"menu\"][data-zen-sheet=\"perm\"],\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > *:not([data-mobile-nav]) > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) {\n position: fixed !important;\n left: 0 !important;\n right: 0 !important;\n bottom: 0 !important;\n top: auto !important;\n transform: none !important;\n width: 100% !important;\n min-width: 0 !important;\n max-width: none !important;\n max-height: min(70dvh, 520px) !important;\n box-sizing: border-box !important;\n border-radius: 16px 16px 0 0 !important;\n border-bottom: none !important;\n padding: 8px 8px calc(8px + var(--mnav-sab)) !important;\n /* 60 for the body-portaled menu too: the host gives it 1100 for desktop\n stacking, but nothing of ours or the host's sits between 60 and the\n sheet on a phone (hit-tested at four points on 0.1.5-rc.2, 2026-09-21),\n and 60 keeps it under the session-info sheet's 70 like the PERM one. */\n z-index: 60 !important;\n box-shadow: 0 -8px 32px rgba(0, 0, 0, .18) !important;\n }\n /* 44pt+ rows in both sheets (Menu items, model options, and the model\n sheet's two root cells that drill into the model / effort panes).\n The model panes are matched by ROLE too: on DSH 0.2.0 the selected\n option's class list ends in `_selected` (`\u2026_option \u2026_selected`), so the\n class-suffix selector missed exactly the checked row \u2014 it fell back to\n the host's 34px pill while its siblings got 48px. The root cells are\n role=menuitem, the options role=menuitemradio; the class forms stay for\n older hosts. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [role=\"menu\"] [role=\"menuitem\"],\n body > div[role=\"menu\"][data-zen-sheet=\"perm\"] [role=\"menuitem\"],\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > *:not([data-mobile-nav]) > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) [role=\"menuitem\"],\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > *:not([data-mobile-nav]) > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) [role=\"menuitemradio\"],\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > *:not([data-mobile-nav]) > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) [class$=\"_option\"],\n :is([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > *:not([data-mobile-nav]) > [class$=\"_menu\"], body > [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) [class$=\"_cell\"] {\n min-height: 48px !important;\n border-radius: 12px !important;\n font-size: 15px !important;\n }\n /* --- 4a. third-party composer entries live in the dock's chip strip ---\n effects/model-sheet-extras.ts collects the composer's secondary controls\n (input.right, input.left, and anything a third party parks directly into\n _tools / _standardControls / _trailing / _row) and moves the REAL nodes\n into [data-mobile-nav=\"composer-chips\"] \u2014 a 26px horizontally\n scrollable strip the effect mounts at the head of the dock rail above\n the card. Always visible, one tap, no sheet in between.\n\n In the row: hidden, but ONLY once the effect has marked them. Two opt-in\n markers, both written by the effect: the input.right CONTAINER gets\n data-zen-sheet-extras (so an emptied wrapper leaves no flex gap), and\n every collected node gets data-zen-composer-extra. Visibility comes back\n the moment a node is parked (=\"open\") or the effect releases it (marker\n removed) \u2014 a version of the effect that declines to act, or never runs\n at all, leaves every control exactly where the host put it; nothing can\n end up hidden from both places at once (the 2026-09-06 review's rule). */\n [data-slot=\"conversation.input.right\"][data-zen-sheet-extras],\n [data-zen-composer-extra]:not([data-zen-composer-extra=\"open\"]) {\n display: none !important;\n }\n /* The strip itself: one 26px line at the rail's head (order 0, document\n order first via the effect's prepend), scrolling sideways when the\n collected controls overflow. Scoped to the dock CHILD \u2014 the generic\n marker also sits on the emptied input.right container in the row, where\n nothing but the opt-in hide above may touch it. This specificity (0,2,0)\n is what beats the section 7 `> *` cage wherever they overlap (overflow,\n max-width, radius are all !important there too). */\n [data-slot=\"conversation.input.dock\"] > [data-mobile-nav=\"composer-chips\"] {\n display: flex !important;\n flex-flow: row nowrap !important;\n align-items: center !important;\n gap: 6px !important;\n order: 0 !important;\n flex: 0 1 auto !important;\n min-width: 0 !important;\n max-width: 100% !important;\n min-height: 26px !important;\n max-height: 26px !important;\n border-radius: 0 !important;\n overflow-x: auto !important;\n overflow-y: hidden !important;\n scrollbar-width: none !important;\n }\n [data-slot=\"conversation.input.dock\"] > [data-mobile-nav=\"composer-chips\"]::-webkit-scrollbar {\n display: none !important;\n }\n /* Parked controls shrink from whatever the row allowed them to the rail's\n 26px pill scale: auto width again (no full-width rows here), tight\n height, and the chip font the rest of the rail uses. */\n [data-mobile-nav=\"composer-chips\"] > * {\n flex: 0 0 auto !important;\n order: 0 !important;\n display: inline-flex !important;\n align-items: center !important;\n min-height: 26px !important;\n max-height: 26px !important;\n width: auto !important;\n max-width: none !important;\n }\n [data-mobile-nav=\"composer-chips\"] button {\n box-sizing: border-box !important;\n height: 26px !important;\n min-height: 26px !important;\n max-height: 26px !important;\n width: auto !important;\n padding: 0 10px !important;\n gap: 5px !important;\n border-radius: 999px !important;\n font-size: 11.5px !important;\n line-height: 18px !important;\n white-space: nowrap !important;\n }\n /* The vision toggle is squeezed to a 28px icon-only button in the row\n (compat.css.ts); in the strip it is a 14px icon + text pill, so its\n label comes back. */\n [data-mobile-nav=\"composer-chips\"] [data-vision-router-mode-toggle] > span {\n display: inline !important;\n }\n [data-mobile-nav=\"composer-chips\"] [data-vision-router-mode-toggle] > svg:first-of-type {\n width: 14px !important;\n height: 14px !important;\n }\n /* The speed chip opens a menu of its own, anchored `bottom: 100%` to its\n trigger. Inside the strip \u2014 which is `overflow-x: auto`, so its content\n clips at the strip's box \u2014 that menu would be cut off. Same escape\n section 4 uses for the host's two menus: position: fixed leaves the clip\n behind and it lands as its own bottom sheet over the composer. */\n [class$=\"_composerSeat\"]:has([data-zen-sheet-extras] [role=\"menu\"]) {\n z-index: 60 !important;\n }\n [data-zen-sheet-extras] [role=\"menu\"] {\n position: fixed !important;\n left: 0 !important;\n right: 0 !important;\n bottom: 0 !important;\n top: auto !important;\n margin: 0 !important;\n width: 100% !important;\n min-width: 0 !important;\n max-width: none !important;\n max-height: min(70dvh, 520px) !important;\n box-sizing: border-box !important;\n border-radius: 16px 16px 0 0 !important;\n border-bottom: none !important;\n padding: 8px 8px calc(8px + var(--mnav-sab)) !important;\n /* Above the page content behind the seat (the seat is raised to 60 by\n the rule above while a parked menu is open), below the session-info\n sheet's 70 (info.css.ts). */\n z-index: 61 !important;\n box-shadow: 0 -8px 32px rgba(0, 0, 0, .18) !important;\n }\n [data-zen-sheet-extras] [role=\"menu\"] [role=\"menuitemradio\"] {\n min-height: 48px !important;\n border-radius: 12px !important;\n font-size: 15px !important;\n }\n\n /* --- 4b. the official scroll-to-bottom button must not poke through the\n sheet (real-device follow-up, 2026-08-17) ---\n ChatView's own \"jump to latest\" button (aria-label t(\"chat.toBottom\"))\n is `position: sticky; z-index: 8` inside the message column, not\n `position: fixed` \u2014 it never escapes to the same top-level stacking\n context our sheets get promoted to, so raising the sheet's z-index\n further does nothing (measured live: it still rendered on top at\n z-index 60 vs 8). Rather than chase engine-specific stacking-context\n semantics (Chromium and WebKit do not always agree here \u2014 see AGENTS.md\n CDP/document.hidden lesson for another instance of that), this hides\n the button outright while either sheet is open and lets it reappear on\n close: a plain `display: none` behind a live `:has()` read is correct\n regardless of which stacking rules the engine happens to apply.\n Selector: no hashed classes (dsh-client-ui-conversation lib/client.js,\n verified 2026-08-17) \u2014 `data-chat-flow` is the one stable attribute on\n the message column, and the button's wrapper is its only sibling\n (ChatView only ever renders the column and, conditionally, this one\n slot), so the adjacent-sibling combinator pins it precisely. */\n body:has([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [role=\"menu\"]) [data-chat-flow] + div,\n body:has([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_standardControls\"] > [data-slot=\"conversation.input.model\"] > *:not([data-mobile-nav]) > [class$=\"_menu\"]) [data-chat-flow] + div,\n body:has(> [id$=\"-menu\"][class$=\"_menu\"]:is([role=\"menu\"], [role=\"group\"])) [data-chat-flow] + div,\n /* T70: the portaled permission menu (marked by effects/permission-sheet.ts). */\n body:has(> div[role=\"menu\"][data-zen-sheet=\"perm\"]) [data-chat-flow] + div,\n /* T-issue30: an inline menu a parked chip opened (escaped to a fixed\n sheet by 4a). */\n body:has([data-zen-sheet-extras] [role=\"menu\"]) [data-chat-flow] + div {\n display: none !important;\n }\n /* --- 5. input box: two lines minimum, five lines maximum ---\n The official autosizer drives the box off the hidden mirror's height, so\n a min-height on the mirror IS the min-height of the field; the scroll cap\n rides the official custom property. 52px = 2 x 24px line box + 4px pad,\n 124px = 5 lines. The hero card keeps its own one-line collapse (misc.css\n pins _scroll/_grow while the placeholder shows). */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] [class$=\"_mirror\"] {\n min-height: 52px !important;\n }\n [class$=\"_composerSeat\"] {\n --dsh-composer-text-max-height: 124px;\n }\n /* Pull the text in from the card's left border. Upstream insets the editing\n host by 16px while the control row below it starts 8px in (row padding,\n section 1), so the placeholder sat noticeably further out than the\n buttons under it and the field read as over-padded on a phone (reported\n 2026-09-06). 12px squares it with the 12px of clear space on the right,\n giving the text a symmetric box, and closes most of the gap with the\n button row. Anchored on the editing host's own contract attribute rather\n than a hashed class. */\n [data-slot=\"conversation.composer.bar\"] [data-composer-input] {\n padding-left: 12px !important;\n }\n\n /* --- 6. no divider above OR below the message list ---\n Instead of a rule the messages butt against, they fade near both edges.\n This used to be ONE mask-image on the message scroller; iOS WebKit\n turned that into fog over the header (user report 2026-08-23): a mask\n on a scroll container is applied in CONTENT coordinates there (the fade\n bands scroll away with the messages) and, worse, it breaks the\n container's own overflow clipping \u2014 content scrolled out of view keeps\n painting, straight over the header and status bar. Engine bug with no\n iOS escape hatch (every iOS browser is WebKit), so the fades moved off\n the scroller entirely:\n\n - fade-IN under the header: the header's own ::after, restyled into a\n 20px gradient strip \u2014 styles/header.css.ts (\"no header bottom line\").\n - fade-OUT above the composer: nothing of ours. The official composer\n seat already carries `background: linear-gradient(transparent 0,\n bg 36px)` on its sticky self (dsh-client-ui-conversation lib/\n client.js .wSkVaW_composerSeat) and that is the entire visible\n effect; the mask's own bottom band landed inside the safe-area\n padding below the card and painted nothing. If the bottom edge ever\n reads harsh, hang a ::before strip off the seat the same way the\n header does \u2014 do NOT put a mask back on the scroller.\n\n Painted strips instead of a mask is a fair trade only because the page\n background is one flat color \u2014 which it is (--dsw-alias-bg-base). */\n [class$=\"_composerSeat\"],\n [class$=\"_composerStack\"] {\n border-top: none !important;\n }\n\n /* --- 7. dock entries become mini chips above the input card ---\n conversation.input.dock is display:contents (inline style), so its\n entries are stacked rows of the composer column. Forcing the slot itself\n to flex turns them into ONE scrollable chip line outside and above the\n card. Written against the slot, not against any particular plugin's chip\n (the git branch chip and the todo panel are third-party and may not be\n installed at all). */\n [data-slot=\"conversation.input.dock\"] {\n display: flex !important;\n flex-flow: row nowrap !important;\n align-items: center !important;\n gap: 6px !important;\n min-width: 0 !important;\n margin: 0 16px !important;\n overflow-x: auto !important;\n overflow-y: hidden !important;\n scrollbar-width: none !important;\n /* The dock is itself a flex ITEM of the composer stack, and the\n new-session hero pins that stack to a fixed height. overflow-x:auto\n above makes this a scroll container, which drops a flex item's\n automatic minimum size to 0 \u2014 so the hero squeezed the dock to ~5px\n and the attachment chips slid under the card drawn after it (only the\n top sliver of a thumbnail showed; reported from a real phone,\n 2026-08-26). Refuse the squeeze: natural height, the stack centers a\n few px lower instead. In-chat the column is not height-pinned and this\n is a no-op. */\n flex-shrink: 0 !important;\n }\n [data-slot=\"conversation.input.dock\"]::-webkit-scrollbar {\n display: none !important;\n }\n [data-slot=\"conversation.input.dock\"] > * {\n flex: 0 0 auto !important;\n max-width: 70% !important;\n min-height: 26px !important;\n max-height: 26px !important;\n border-radius: 999px !important;\n font-size: 11.5px !important;\n line-height: 18px !important;\n overflow: hidden !important;\n }\n /* The git-graph chip carries a 34px tablet target from misc.css; that\n selector is more specific, so restate it at the phone breakpoint. */\n [data-slot=\"conversation.input.dock\"] [data-gitgraph-chip-anchor] [data-gitgraph-chip] {\n min-height: 26px !important;\n padding: 0 10px !important;\n font-size: 11.5px !important;\n }\n /* --- 7a. the native to-do card opts OUT of the 26px pill cage ---\n conversation.input.dock is not only a chip rail: DSH's own TodoPanel\n (dsh-client-ui-conversation) registers here, and it is an expandable\n card, not a pill \u2014 tapping its header renders the to-do list INSIDE\n itself. The cage's max-height:26px + overflow:hidden left that list\n rendered but clipped to nothing, so on a phone the bar looked dead:\n aria-expanded flipped, 8 rows mounted at 216px, and the card still\n painted 26px tall (measured live, 2026-08-20). Give it the official\n geometry back \u2014 full row, natural height, 12px radius. Its list caps\n itself at 180px with its own scroller, so this cannot run into the\n composer. Anchored on the official data-testid, a stable contract\n marker rather than a hashed class name. */\n [data-slot=\"conversation.input.dock\"] > [data-testid=\"todo-panel\"] {\n /* A whole line to itself. It shared line 0 with the pills until the goal\n bar turned up (2026-09-06): two full-width things on one nowrap line\n meant the to-do header shrank to a stub and the goal bar ran off the\n right edge. Both are status strips you read at a glance, so they stack\n \u2014 see the line policy in 7c. */\n flex: 1 0 100% !important;\n order: 1 !important;\n min-width: 0 !important;\n max-width: none !important;\n min-height: 0 !important;\n max-height: none !important;\n overflow: visible !important;\n border-radius: 12px !important;\n /* The cage shrinks dock text to 11.5px/18px; the card's rows inherit it\n and turn cramped. Restore the official 13px body scale. */\n font-size: 13px !important;\n line-height: normal !important;\n }\n\n /* --- 7b. attachment chips (S7.1) ---\n Our own dock entry opts OUT of the 26px pill cage above: an image chip is\n a 48px tile. The row sits tight under the card's top edge, so the chips\n read as part of the composer rather than as a floating strip.\n\n It also takes the LAST line of its own (line policy in 7c): a 48px tile\n beside the to-do chip on one nowrap line squeezed that chip down to an\n unreadable stub, with a long file name pushing the rest off-screen\n (reported 2026-08-22). Its own chips wrap rather than overflow, so\n several attachments stack instead of scrolling out of reach. */\n [data-slot=\"conversation.input.dock\"] > [data-mobile-nav=\"attach-chips\"] {\n display: flex !important;\n flex: 1 0 100% !important;\n flex-wrap: wrap !important;\n order: 4 !important;\n align-items: center !important;\n gap: 6px !important;\n max-width: none !important;\n min-height: 0 !important;\n max-height: none !important;\n border-radius: 0 !important;\n overflow: visible !important;\n margin-bottom: -2px;\n }\n\n /* --- 7b2. the T34 remote-status banner opts OUT of the cage ---\n Same class of entry as the to-do card (7a): a full-width status strip,\n not a pill. dsh-zen-remote's remote banner registers into this dock at\n order 15 when a remote session is offline or closed (T34) \u2014 the 26px\n cage would clip the copy to one unreadable sliver. A whole line of its\n own, natural height. */\n [data-slot=\"conversation.input.dock\"] > [data-zen-remote=\"remote-banner\"] {\n flex: 1 0 100% !important;\n order: 2 !important;\n max-width: none !important;\n min-height: 0 !important;\n max-height: none !important;\n overflow: visible !important;\n border-radius: 12px !important;\n font-size: 12px !important;\n line-height: 18px !important;\n }\n\n /* --- 7c. the queue strip opts OUT of the cage, on a line of its own ---\n Third entry that is not a pill. conversation.input.dock is a `list` slot\n and DSH's own QueueDock registers into it at order 20 (id `queue`,\n dsh-client-ui-conversation) \u2014 a 36px row per queued message, or a\n collapsible \"N \u6761\u6392\u961F\u6D88\u606F\" header once there is more than one. The 26px\n cage above clipped it to a sliver: on a phone a queued message showed as\n a grey stub poking out from behind the input card, unreadable and with\n its edit / delete / send buttons cut off (reported 2026-08-22, same\n failure the to-do card hit in 7a).\n\n Two things are restored. Geometry: the official row height back, and the\n dock's own composer-column sizing dropped \u2014 its width/max-width are\n calc()s over the composer card variables and its bottom margin is\n negative by design (upstream tucks the strip UNDER the card), none of\n which survives being a flex item of our chip rail.\n\n Layout \u2014 the line policy for the whole rail, now that three things want\n room on it. The pills (the to-do card, a branch chip, whatever else\n registers) keep order 0 and share the top line, still scrolling\n horizontally. The queue takes the next line (order 1). The attachment\n chips take the last one (order 2, 7b), so what the user is about to send\n sits closest to the input it will be sent from. The rail only wraps\n while one of the two full-width entries exists, so a session with\n neither keeps the original single scrolling chip line. */\n [data-slot=\"conversation.input.dock\"]:has(> [data-queue-dock], > [data-mobile-nav=\"attach-chips\"], > [data-testid=\"todo-panel\"], > [data-goal-bar]) {\n flex-wrap: wrap !important;\n }\n [data-slot=\"conversation.input.dock\"] > [data-queue-dock] {\n flex: 1 0 100% !important;\n order: 3 !important;\n max-width: none !important;\n min-height: 0 !important;\n max-height: none !important;\n margin: 0 !important;\n padding: 0 !important;\n border-radius: 0 !important;\n overflow: visible !important;\n font-size: 13px !important;\n line-height: normal !important;\n }\n /* dsh-client-ui-goal's status strip (its own `data-goal-bar` marker; it\n registers into this dock at order 10). Fourth thing here that is not a\n pill: a row carrying the objective text plus pause / edit / delete\n buttons, so the 26px cage clipped its buttons off the right edge and the\n nowrap line pushed what was left off screen (reported 2026-09-06). Same\n treatment the queue gets \u2014 a line of its own, official geometry back. */\n [data-slot=\"conversation.input.dock\"] > [data-goal-bar] {\n flex: 1 0 100% !important;\n order: 2 !important;\n max-width: none !important;\n min-height: 0 !important;\n max-height: none !important;\n border-radius: 12px !important;\n overflow: visible !important;\n font-size: 13px !important;\n line-height: normal !important;\n }\n\n /* The panel is the dock's only child; upstream rounds its top corners only\n and drops the bottom border, because there it is half-hidden behind the\n card. On its own line it is a free-standing strip, so close it up. */\n [data-slot=\"conversation.input.dock\"] > [data-queue-dock] > * {\n border-radius: 12px !important;\n }\n [data-slot=\"conversation.input.dock\"] > [data-queue-dock] > *::after {\n border-bottom: 1px solid var(--dsw-alias-border-l1) !important;\n }\n\n [data-mobile-nav=\"attach-chip\"] {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n flex: 0 0 auto;\n max-width: 62vw;\n background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .12));\n color: var(--dsw-alias-label-primary, inherit);\n }\n [data-mobile-nav=\"attach-chip\"][data-kind=\"file\"] {\n height: 26px;\n padding: 0 4px 0 8px;\n border-radius: 999px;\n font-size: 11.5px;\n line-height: 18px;\n }\n /* An image chip is the thumbnail: no name line, the filename lives in the\n title attribute (and in the draft text right below). */\n [data-mobile-nav=\"attach-chip\"][data-kind=\"image\"] {\n width: 48px;\n height: 48px;\n padding: 0;\n border-radius: 10px;\n overflow: hidden;\n }\n [data-mobile-nav=\"attach-chip-art\"] {\n position: relative;\n display: grid;\n place-items: center;\n flex: none;\n overflow: hidden;\n }\n [data-kind=\"file\"] > [data-mobile-nav=\"attach-chip-art\"] {\n width: 16px;\n height: 16px;\n opacity: .7;\n }\n [data-kind=\"image\"] > [data-mobile-nav=\"attach-chip-art\"] {\n width: 100%;\n height: 100%;\n }\n /* The <img> is stacked over the paperclip, not swapped for it: a format the\n engine cannot decode (HEIC outside WebKit) simply paints nothing and the\n icon shows through \u2014 no onError state to carry. */\n [data-mobile-nav=\"attach-chip-art\"] img {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n background: var(--dsw-alias-bg-layer-2, rgba(127, 127, 127, .18));\n }\n [data-mobile-nav=\"attach-chip-name\"] {\n min-width: 0;\n overflow: hidden;\n white-space: nowrap;\n }\n [data-mobile-nav=\"attach-chip-remove\"] {\n display: grid;\n place-items: center;\n flex: none;\n width: 18px;\n height: 18px;\n padding: 0;\n border: none;\n border-radius: 999px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n touch-action: manipulation;\n }\n /* On a tile the \u00D7 floats in the corner over the picture, so it needs its own\n ground to stay legible against an arbitrary photo. */\n [data-kind=\"image\"] > [data-mobile-nav=\"attach-chip-remove\"] {\n position: absolute;\n top: 2px;\n right: 2px;\n background: rgba(0, 0, 0, .55);\n color: #fff;\n }\n [data-mobile-nav=\"attach-chip-remove\"]:active {\n transform: scale(.9);\n }\n\n /* --- 8. the official stats strip leaves the composer ---\n Its data moves into the session info card (S4). The row is the composer\n dock's own `_root` entry; the slot itself stays live for later entries. */\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"],\n [data-slot=\"conversation.composer.dock\"] > [data-composer-stat] {\n display: none !important;\n }\n\n /* --- 9. the attachment button (S7; its file input is hidden at the top of\n this file, at every width) --- */\n [data-mobile-nav=\"attach\"] {\n position: relative;\n width: 28px !important;\n height: 28px !important;\n flex: none !important;\n display: grid !important;\n place-items: center !important;\n padding: 0 !important;\n border: none !important;\n border-radius: 999px !important;\n background: var(--dsw-specific-selector, rgba(127, 127, 127, .12));\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n touch-action: manipulation;\n }\n [data-mobile-nav=\"attach\"]:active {\n transform: scale(.94);\n transition: transform .12s;\n }\n /* Busy: a ring sweeps around the paperclip. Drawn with a conic gradient in\n a pseudo-element rather than a spinner node, so the official React tree\n around us has nothing extra to re-render (same reasoning as the model\n pill's mask icon, S3.1). */\n [data-mobile-nav=\"attach\"][data-busy]::after {\n content: \"\";\n position: absolute;\n inset: -2px;\n border-radius: 999px;\n background: conic-gradient(from 0deg, transparent 0 65%, currentColor 100%);\n mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));\n -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));\n animation: mnav-attach-spin .9s linear infinite;\n pointer-events: none;\n }\n @keyframes mnav-attach-spin {\n to { transform: rotate(1turn); }\n }\n @media (prefers-reduced-motion: reduce) {\n [data-mobile-nav=\"attach\"][data-busy]::after {\n animation-duration: 3s;\n }\n }\n /* Failure note: one line above the button, tap-to-dismiss. Sits on the\n composer card (z 2) rather than in an overlay layer \u2014 nothing here needs\n to clear the permission/model sheets. */\n [data-mobile-nav=\"attach-error\"] {\n position: absolute;\n bottom: calc(100% + 6px);\n left: 0;\n z-index: 2;\n max-width: 62vw;\n padding: 5px 8px;\n border-radius: 8px;\n font-size: 12px;\n line-height: 1.3;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, .82));\n color: var(--dsw-alias-label-primary, #fff);\n box-shadow: 0 2px 8px rgba(0, 0, 0, .24);\n }\n\n /* --- 9. home-indicator clearance (S4.1, 2026-08-17) ---\n Owned HERE, not by the gateway half. That plugin used to carry\n [data-slot=\"conversation.composer\"] { padding-bottom: max(env(safe-area-inset-bottom), 8px) }\n in both pwa/app.css and the gateway's inline DEVICE_CSS. Both were\n INERT and had always been: that slot element is `display: contents`\n (measured 2026-08-17 \u2014 the slot wrapper generates no box, so padding on\n it is discarded), which is why raising it never moved anything. The rule\n is deleted on the PWA side and restated here on an element that actually\n lays out, reading --mnav-sab so ?mobile-nav-inset=54,34 can regress it\n off-device (env() is hard 0 on desktop \u2014 the whole reason S2.1 exists).\n\n Target: the card should look EQUALLY inset on all four sides. This same\n element carries the card's side inset (`padding: 0 16px` officially,\n measured 16px left and 16px right at 390px), so the bottom gap is simply\n capped at that same 16px. Clearing the FULL 34px inset \u2014 the naive\n max(sab, 8px) \u2014 is what read as \"too thick\": it is more than double the\n side margin, so the card looked shoved up off the bottom edge.\n\n clamp() says all three requirements at once:\n - floor 8px -> a device with no home indicator (sab: 0) keeps the\n official 8px exactly, so this is a strict no-op there;\n - track sab -> a shallower inset than 16px is honoured as-is;\n - cap 16px -> a full-size indicator (sab: 34) lands on 16px, equal\n to the side inset, which is the look being asked for.\n Keep the 16px in step with the side padding above if that ever changes;\n that equality IS the spec here, not a coincidence. */\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"] {\n padding-bottom: clamp(8px, var(--mnav-sab), 16px) !important;\n }\n\n /* S10 keyboard avoid: effects/keyboard-avoid.ts mirrors the band of the\n layout viewport hidden behind the software keyboard into --mnav-kb-lift\n and stamps the html attribute while (and only while) it is non-zero \u2014\n a resting transform, even translateY(0), would silently become the\n containing block for any fixed-position descendant of the bar. In every\n environment where the browser's own focus-reveal works the attribute is\n simply never set.\n The target is the slot's _root CHILD, not the slot wrapper: the wrapper\n is display:contents \u2014 it generates no box, so a transform on it is a\n silent no-op (found live on-device via CDP, 2026-08-21; the variable and\n attribute were set and nothing moved). */\n html[data-mnav-kb] [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"] {\n transform: translateY(calc(-1 * var(--mnav-kb-lift, 0px)));\n }\n}\n\n/* The attachment button and its preview row only exist for the phone shell.\n Both render at every width (the slots are not breakpoint-aware), so both\n need hiding here \u2014 the dock-row rules that shape the chips live in the\n < 768px block and would otherwise leave a bare, unstyled chip line in the\n desktop composer. */\n@media (min-width: 768px) {\n [data-mobile-nav=\"attach\"],\n [data-mobile-nav=\"attach-chips\"] {\n display: none !important;\n }\n}\n";
2
2
  //# sourceMappingURL=composer.css.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"composer.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/composer.css.ts"],"names":[],"mappings":"AAkDA,eAAO,MAAM,YAAY,yriDAk3BxB,CAAA"}
1
+ {"version":3,"file":"composer.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/composer.css.ts"],"names":[],"mappings":"AAoEA,eAAO,MAAM,YAAY,yrhDA25BxB,CAAA"}
@@ -1,2 +1,2 @@
1
- export declare const HOME_CSS = "/* ---------- phone app shell (< 768px) ---------- */\n\n@media (max-width: 767px) {\n /* --- the document never scrolls (S1.1, 2026-08-17) ---\n Real-device symptom: the workspace title bar, the session header, the\n composer and the FAB all slid with the finger \u2014 every fixed surface\n \"followed the drag\". They are not misplaced; the whole DOCUMENT was\n rubber-banding under them (they are absolute/in-flow inside the frame,\n so a document-level bounce moves them all as one).\n\n After the S2.1 box-sizing fix the document has no overflow at all\n (scrollHeight === clientHeight, measured with ?mobile-nav-inset=54), so\n this is not scrolling \u2014 it is iOS's elastic overscroll, which happens on\n an unscrollable document too, and which an inner scroller chains into as\n soon as it hits its own end.\n\n overscroll-behavior: none on the viewport kills both halves at once: the\n document itself gets no bounce, and overscroll chained up from the\n message flow / session list is absorbed without moving anything.\n overflow: hidden then hard-locks the document scroller so future content\n can never reintroduce a real scroll. Set on html AND body: the spec\n propagates the viewport's value from html, but engines have historically\n read body, and neither is a scroll container we ever want.\n\n Deliberately NOT position: fixed on body \u2014 the app shell does not need\n it, and it is the variant that strands iOS's fixed elements behind the\n on-screen keyboard. With plain overflow: hidden, iOS still pans the\n visual viewport to reveal a focused textarea, so the composer stays\n visible while typing without any visualViewport JS. */\n html,\n body {\n overflow: hidden !important;\n overscroll-behavior: none !important;\n }\n\n /* The message flow is the one scroller that regularly hits its end under a\n finger. Root-level `none` already absorbs the chain, but declaring it\n at the source keeps the guarantee if the root rule is ever weakened\n (the home list and both sheets already declare it).\n\n `none`, NOT `contain` (S4.1 fix, 2026-08-17). The two differ in exactly\n the way that bit us: `contain` stops overscroll from CHAINING OUT to the\n document but deliberately KEEPS this element's own local elastic bounce,\n while `none` suppresses that local bounce too. S1.1 only reasoned about\n the chain, so it picked `contain` \u2014 and the composer went on twitching\n upward on every drag-past-the-end while the header and FAB stayed put.\n\n Why only the composer moved: the official composer seat\n (`[class$=\"_composerSeat\"]`) is `position: sticky; bottom: 0` and lives\n INSIDE this scroller (measured 2026-08-17: seat 0,710 390x134 inside\n _scrollBody 0,131 390x713, contains() === true). A sticky box is laid out\n against its scroll container's content, so the container's own rubber-band\n drags it along. The header/FAB are absolute on the frame, outside this\n scroller entirely \u2014 which is precisely why S1.1's document-level fix\n pinned them and left this one surface behind.\n\n Deliberately NOT re-pinning the seat with position: fixed: sticky inside\n the scroller is what keeps the focused textarea visible when iOS pans the\n visual viewport for the keyboard, and fixed is the variant that strands\n it behind the keyboard (same trap as the body rule above). Kill the\n bounce, keep the sticky. */\n [data-phase] [class$=\"_scrollBody\"] {\n overscroll-behavior: none !important;\n }\n\n /* The official sidebar is no longer a drawer on a phone \u2014 the home screen\n replaced it. Hidden outright (not translated off-screen) so it cannot\n capture taps or hold layout. */\n [data-mobile-nav=\"frame\"] > :first-child {\n display: none !important;\n }\n /* The header's drawer toggle opened that sidebar; with it gone the button\n has nothing to open. (S2 owns the rest of the header layout \u2014\n styles/header.css.ts \u2014 including the Files button's replacement.) */\n [data-mobile-nav=\"toggle\"] {\n display: none !important;\n }\n\n /* --- level 1: the session list ---\n Renders inside the shell overlay layer (absolute, pointer-events: none),\n so the page re-enables pointer events for itself. The layer's containing\n block is the frame's padding box, which still starts under the status\n bar \u2014 hence the safe-area padding here. */\n [data-mobile-nav=\"home\"] {\n position: absolute;\n inset: 0;\n /* rc.3 raise (real-device report 2026-10-25 \"top strip leaking the\n conversation through\"): this shell layer mounts INSIDE the host's\n PIRFFq_overlayLayer (z-index:20, new in rc.3 \u2014 the conversation\n content itself moved under it too), so the old z:5 let host session\n content (STRONG/CODE rows measured at viewport y=4) paint over the\n shell's top bar on BOTH pages, reading as a faint fixed strip at the\n window top. z:30 puts the whole shell above everything the host\n stacks inside that layer; the shell's own transparent middle and\n pointer-events passthrough keep the conversation visible and tappable\n (verified live via console injection before landing). */\n z-index: 30;\n display: flex;\n flex-direction: column;\n pointer-events: auto;\n /* dsw-specific-sidebar-fill, not an alias bg-layer-* token (real-device\n round 2 follow-up, 2026-08-17): fetched and diffed the live theme\n CSS (both light/dark blocks in /assets/index-*.css) because computed\n values, not source-read guesses, are what actually matter here \u2014\n --dsw-alias-bg-base/-layer-1/-layer-2 all resolve to the exact same\n color in the LIGHT theme (neutral-bluish-00, i.e. plain white); they\n only diverge in dark mode.\n\n 2026-08-20: the page and the CARDS swapped tokens. This surface used\n to be --dsw-specific-sidebar-fill, which made the list page a\n different shade from the session page \u2014 measured on-device in dark\n theme, rgb(27,27,28) here against rgb(21,21,23) there, a visible step\n every time you opened a session. The page now takes bg-base, the same\n token the session page and <body> resolve to, so the two screens are\n literally the same colour; the cards took sidebar-fill in exchange, so\n the contrast that separates a row from the page is preserved \u2014 it\n just runs the other way (raised card on a base page, the more\n conventional reading of the two anyway). */\n background: var(--dsw-alias-bg-base, #ffffff);\n color: var(--dsw-alias-label-primary, inherit);\n padding-top: var(--mnav-sat);\n transform: translateX(0);\n opacity: 1;\n transition:\n transform .3s var(--ds-ease-in-out, ease-in-out),\n opacity .3s var(--ds-ease-in-out, ease-in-out);\n }\n /* Push transition: the list slides out to the left as the session takes\n the screen, and comes back from the left. It stays mounted (visibility,\n not display) so the slide has something to animate; the delayed\n visibility keeps it non-interactive the moment the transition ends. */\n [data-mobile-nav=\"home\"][data-view=\"session\"] {\n transform: translateX(-100%);\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n transition:\n transform .3s var(--ds-ease-in-out, ease-in-out),\n opacity .3s var(--ds-ease-in-out, ease-in-out),\n visibility 0s .3s;\n }\n /* The host's SESSION header must not sit on top of this list page.\n Real-device report (2026-10-22, DSH 0.2.0-rc.2, dsh-web-mobile 3.2.1,\n 360x792 phone): on the list page the top strip showed the LAST SESSION's\n header \u2014 its title plus the session's tab row \u2014 and the workspace switcher\n underneath was neither visible nor tappable, so \"switch workspace\" did\n nothing at all. Measured with elementFromPoint(150,30) on the phone\n viewport: span.wSkVaW_crumbCurrent, i.e. the host header's crumb, while\n our own workspace button sat at [46,8,208,44] underneath it.\n\n Why z-index is not the fix: this page is position:absolute; z-index:5\n inside the shell overlay layer, and the host header carries its own\n stacking context, so raising us above its z-index:30 changed nothing\n (measured: the crumb still won the hit test). The session header has to\n leave the hit tree while the list page is up \u2014 the list page draws its\n own top bar (home-top, with the workspace switcher).\n\n The gate is this page's own marker: data-view=\"session\" means a session\n is open, so the rule lapses exactly when the host header is wanted again\n (verified: entering a session restores display:grid, the back button in\n that header stays tappable, and going back hides it once more). Two\n selectors on purpose \u2014 the class suffix survives CSS-module hash renames,\n the structural one survives a rename of the class itself. */\n [data-mobile-nav=\"frame\"]:not([data-mobile-view=\"session\"]) header[class*=\"_header\"],\n [data-mobile-nav=\"frame\"]:not([data-mobile-view=\"session\"]) header:has([class*=\"_titleRow\"]),\n body:has([data-mobile-nav=\"home\"]:not([data-view=\"session\"])) [data-mobile-nav=\"frame\"] header[class*=\"_header\"],\n body:has([data-mobile-nav=\"home\"]:not([data-view=\"session\"])) [data-mobile-nav=\"frame\"] header:has([class*=\"_titleRow\"]) {\n display: none !important;\n }\n @media (prefers-reduced-motion: reduce) {\n [data-mobile-nav=\"home\"],\n [data-mobile-nav=\"home\"][data-view=\"session\"] {\n transition: none !important;\n }\n }\n\n /* Title bar: the workspace name IS the switcher. */\n [data-mobile-nav=\"home-top\"] {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n min-height: 52px;\n padding: 4px 16px 6px;\n }\n /* Site logo (real-device round 2, 2026-08-17): rendered only when\n document.head actually has a <link rel=\"icon\"> (MobileHome.tsx never\n ships a placeholder box), so this rule only ever needs to size the\n image, not reserve space for its absence. */\n [data-mobile-nav=\"home-logo\"] {\n width: 24px;\n height: 24px;\n margin-right: 6px;\n flex: none;\n border-radius: 6px;\n object-fit: contain;\n }\n /* Dark theme (real-device report, 2026-08-17): the runtime favicon is a\n dark-on-transparent whale mark, illegible on the dark page. Force a\n white silhouette regardless of the source image's own colors \u2014\n brightness(0) collapses every opaque pixel to black first, invert(1)\n then flips that to white; transparent pixels stay transparent either\n way. Same body[data-ds-dark-theme] marker as the home-row hairline\n above (AGENTS.md: prefers-color-scheme never fires, the app switches\n themes via this attribute with color-scheme hardcoded to light). */\n body[data-ds-dark-theme] [data-mobile-nav=\"home-logo\"] {\n filter: brightness(0) invert(1);\n }\n [data-mobile-nav=\"ws-switch\"] {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n min-height: 44px;\n max-width: 100%;\n padding: 0 6px 0 0;\n border: none;\n background: transparent;\n color: inherit;\n font-family: inherit;\n font-size: 22px;\n font-weight: 600;\n line-height: 1.2;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"ws-switch\"] > span {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n [data-mobile-nav=\"ws-switch\"] > svg {\n flex: none;\n opacity: .55;\n }\n [data-mobile-nav=\"ws-switch\"]:active {\n opacity: .6;\n }\n\n /* Session list: rounded cards, not bordered rows (real-device round 2\n follow-up, 2026-08-17 \u2014 reference: Claude Code mobile app's session\n list). `gap` on the flex column IS the inter-card spacing; no\n per-row margin bookkeeping. */\n [data-mobile-nav=\"home-list\"] {\n flex: 1 1 auto;\n min-height: 0;\n margin: 0;\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px 16px calc(var(--mnav-sab) + 96px);\n list-style: none;\n overflow-y: auto;\n overscroll-behavior: contain;\n -webkit-overflow-scrolling: touch;\n }\n /* Swipe-to-archive wrapper (redone 2026-08-25, real-device round): iOS-mail\n mechanics. The wrapper CLIPS (overflow hidden, same 22px radius as the\n card) and the archive button is parked past the right edge, sliding in\n lock-step with the card \u2014 a 1px drag reveals a 1px slice, never a\n fully-drawn button behind a gap. Clipping would eat the card's cast\n shadow, so the light-theme shadow moves up to this wrapper (identical\n values; the dark theme's INSET hairline on the card survives clipping\n untouched). pan-y keeps vertical list scrolling native; the direction\n lock in MobileHome.tsx decides horizontal-vs-vertical once per touch. */\n [data-mobile-nav=\"home-swipe\"] {\n position: relative;\n overflow: hidden;\n border-radius: 22px;\n /* The list is a flex column: without this, an overflowing list SHRINKS\n every row and the clip above slices the squashed card's bottom edge\n flat (real-device report, 2026-08-25). Rows keep natural height; the\n list scrolls. */\n flex: 0 0 auto;\n touch-action: pan-y;\n box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 4px 12px rgba(0, 0, 0, .05);\n }\n body[data-ds-dark-theme] [data-mobile-nav=\"home-swipe\"] {\n box-shadow: none;\n }\n /* Press = settle toward the page (mirrors the card's own :active swap,\n which the clip made invisible at wrapper level). */\n [data-mobile-nav=\"home-swipe\"]:has([data-mobile-nav=\"home-row\"]:active) {\n box-shadow: 0 1px 2px rgba(0, 0, 0, .05);\n }\n body[data-ds-dark-theme] [data-mobile-nav=\"home-swipe\"]:has([data-mobile-nav=\"home-row\"]:active) {\n box-shadow: none;\n }\n [data-mobile-nav=\"home-swipe-card\"] {\n position: relative;\n z-index: 1;\n transition: transform .22s var(--ds-ease-out, ease-out);\n will-change: transform;\n }\n /* Inset pill, not a full-bleed slab (real-device feedback: hard edges).\n 5px breathing room on three sides inside the 88px reveal; its own\n radius echoes the card family. It still slides in lock-step from past\n the right edge (transform driven from MobileHome.tsx). */\n [data-mobile-nav=\"home-archive\"] {\n position: absolute;\n z-index: 0;\n top: 5px;\n bottom: 5px;\n right: 5px;\n width: 78px;\n border: none;\n border-radius: 17px;\n background: var(--dsw-alias-state-error-primary, #d4494c);\n color: #fff;\n font-size: 13px;\n font-weight: 600;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n transition: transform .22s var(--ds-ease-out, ease-out);\n will-change: transform;\n }\n [data-mobile-nav=\"home-archive\"]:active {\n filter: brightness(.9);\n }\n\n [data-mobile-nav=\"home-row\"] {\n display: flex;\n align-items: center;\n gap: 12px;\n width: 100%;\n min-height: 60px;\n padding: 14px 16px;\n border: none;\n border-radius: 22px;\n /* The page's old colour (see the swap note on [data-mobile-nav=\"home\"]):\n differs from bg-base in BOTH themes, and is the token\n dsh-better-sidebar's panel already uses for a secondary surface. */\n background: var(--dsw-specific-sidebar-fill, #f5f5f5);\n color: inherit;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n /* Card edge definition (2026-08-17). Light theme only \u2014 see the dark\n override below. Two stacked shadows in the iOS idiom: a tight 1px\n contact shadow that draws the edge itself, plus a wide soft one that\n lifts the card off the page. Both are deliberately weak (.06/.05):\n the page sits on --dsw-specific-sidebar-fill and the card on\n --dsw-alias-bg-base, so there is already a slight tonal step here and\n the shadow only has to sharpen it, not carry it alone. */\n box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 4px 12px rgba(0, 0, 0, .05);\n }\n [data-mobile-nav=\"home-row\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n /* Press = settle toward the page. Dropping the wide shadow and keeping a\n tighter contact one reads as the card pushing in, which is what the\n darkening background already implies; leaving the lifted shadow under\n a pressed card is the combination that looks wrong. */\n box-shadow: 0 1px 2px rgba(0, 0, 0, .05);\n }\n /* Dark theme: a black shadow on a near-black page is invisible, so the\n edge is drawn instead of cast. A 1px hairline at --dsw-alias-border-l2\n weight, delivered as an INSET box-shadow rather than a real border so\n the card's geometry is untouched (a real 1px border would grow every\n row by 2px unless box-sizing cooperated) and so it overrides the light\n shadow through the same property instead of fighting it.\n\n Chosen over \"brighten the card face\" because in dark theme the card\n (--dsw-alias-bg-base, bluish-950) is already DARKER than the page\n (--dsw-specific-sidebar-fill, bluish-900); lightening the card past the\n page would invert the layering that the light theme establishes, while\n a hairline keeps both themes reading as \"page behind, card in front\".\n\n Theme is read from body[data-ds-dark-theme], never prefers-color-scheme:\n the app hardcodes color-scheme: light on <html> and switches themes with\n this attribute, so a media query would never fire (AGENTS.md). This\n selector also outranks the :active rule above (0,2,1 vs 0,2,0), so one\n declaration covers the pressed state too. */\n body[data-ds-dark-theme] [data-mobile-nav=\"home-row\"] {\n box-shadow: inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(255, 255, 255, .12));\n }\n [data-mobile-nav=\"home-row\"][data-current] [data-mobile-nav=\"home-row-title\"] {\n font-weight: 600;\n }\n /* Avatar: a running/warning/done session shows its existing StateDot\n (same component, same semantics as the old inline dot \u2014 just bigger\n and re-homed); otherwise the title's own first character stands in\n for it, so every row has a mark even when idle. interactive-bg-hover,\n not a bg-layer-* token: same reason as the page background above \u2014\n bg-layer-2 is IDENTICAL to the card's own bg-base in light theme\n (verified against the live theme CSS), so it would render invisible\n there. interactive-bg-hover is a translucent rgba tint rather than a\n solid layer color, so it always reads as \"a shade over the card\" in\n both themes regardless of what the card's base color resolves to. */\n [data-mobile-nav=\"home-row-avatar\"] {\n flex: none;\n display: grid;\n place-items: center;\n width: 40px;\n height: 40px;\n border-radius: 12px;\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .55));\n font-size: 16px;\n font-weight: 600;\n }\n [data-mobile-nav=\"home-row-body\"] {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n [data-mobile-nav=\"home-row-title\"] {\n font-size: 16px;\n line-height: 21px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n [data-mobile-nav=\"home-row-status\"] {\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-size: 12.5px;\n line-height: 17px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n [data-mobile-nav=\"home-row-time\"] {\n flex: none;\n align-self: flex-start;\n color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .4));\n font-size: 12.5px;\n line-height: 18px;\n }\n [data-mobile-nav=\"home-empty\"] {\n margin: 12px 16px;\n padding: 48px 24px;\n border-radius: 22px;\n /* Stands in for the rows, so it carries the rows' surface. */\n background: var(--dsw-specific-sidebar-fill, #f5f5f5);\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-size: 15px;\n text-align: center;\n }\n\n /* New-session FAB: a labeled pill (real-device round 2 follow-up), not a\n bare circle \u2014 tap starts in the shown workspace, long press picks\n one. Inverted-surface tokens (not a hardcoded accent color): the same\n pair the official git-commit button in dsh-better-sidebar uses for\n its own solid CTA. */\n [data-mobile-nav=\"home-fab\"] {\n position: absolute;\n right: 18px;\n bottom: calc(var(--mnav-sab) + 22px);\n z-index: 6;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 48px;\n padding: 0 20px 0 16px;\n border: none;\n border-radius: 999px;\n background: var(--dsw-alias-button-primary-fill, #1a1a1a);\n color: var(--dsw-alias-label-primary-inverted, #ffffff);\n font-family: inherit;\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n box-shadow: 0 6px 20px rgba(0, 0, 0, .24);\n touch-action: manipulation;\n user-select: none;\n -webkit-user-select: none;\n -webkit-touch-callout: none;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"home-fab\"]:active {\n transform: scale(.96);\n }\n\n /* Workspace sheet (switcher and long-press New Session share it). */\n [data-mobile-nav=\"home-sheet-layer\"] {\n position: absolute;\n inset: 0;\n z-index: 7;\n }\n [data-mobile-nav=\"home-sheet-mask\"] {\n position: absolute;\n inset: 0;\n background: var(--dsw-alias-bg-mask-3, rgba(0, 0, 0, .45));\n border: none;\n animation: dsh-mobile-nav-fade .18s var(--ds-ease-out, ease-in-out);\n }\n /* Docked to the screen edge, matching the composer's permission/model\n sheets (styles/composer.css.ts section 4) rather than floating.\n\n It used to sit at `bottom: calc(var(--mnav-sab) + 8px)` with all four\n corners rounded, which left a 42px strip of bare mask under it at\n sab: 34 (measured). Docking removes the strip instead of dimming it, and\n the safe area moves INSIDE the card as bottom padding \u2014 so the white\n card body runs all the way to the physical screen edge and the last row\n still clears the home indicator. Same reason the corners go top-only:\n a rounded bottom corner on a card flush with the screen edge reads as a\n rendering mistake. */\n [data-mobile-nav=\"home-sheet\"] {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n max-height: 70%;\n overflow-y: auto;\n overscroll-behavior: contain;\n box-sizing: border-box;\n padding: 6px 6px calc(var(--mnav-sab) + 14px);\n border-radius: 20px 20px 0 0;\n /* layer-2, not bg-base: in dark mode the sheet must lift off a page that\n shares bg-base, or only the shadow separates them. */\n background: var(--dsw-alias-bg-layer-2, #ffffff);\n box-shadow: 0 -8px 32px rgba(0, 0, 0, .28);\n animation: dsh-mobile-nav-sheet-up .22s var(--ds-ease-out, ease-in-out);\n }\n @media (prefers-reduced-motion: reduce) {\n [data-mobile-nav=\"home-sheet-mask\"],\n [data-mobile-nav=\"home-sheet\"] {\n animation: none !important;\n }\n }\n [data-mobile-nav=\"home-sheet-title\"] {\n padding: 10px 12px 6px;\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-size: 13px;\n }\n [data-mobile-nav=\"home-sheet-item\"] {\n display: flex;\n align-items: center;\n width: 100%;\n min-height: 48px;\n padding: 0 12px;\n border: none;\n border-radius: 12px;\n background: transparent;\n color: inherit;\n font-family: inherit;\n font-size: 16px;\n text-align: left;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"home-sheet-item\"][data-selected] {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n font-weight: 600;\n }\n [data-mobile-nav=\"home-sheet-item\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n }\n\n /* --- hero fallback back button ---\n The hero (new blank session) page renders no conversation.session.header,\n so the header-slot back button (S2) does not exist there. This floating\n fallback covers that page and hides itself as soon as the real header\n back mounts. */\n [data-mobile-nav=\"hero-back\"] {\n position: absolute;\n top: calc(var(--mnav-sat) + 8px);\n left: 8px;\n z-index: 6;\n width: 44px;\n height: 44px;\n display: grid;\n place-items: center;\n border: none;\n border-radius: 14px;\n background: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n pointer-events: auto;\n }\n [data-mobile-nav=\"hero-back\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n }\n [data-mobile-nav=\"frame\"]:has([data-mobile-nav=\"header-back\"]) ~ [data-mobile-nav=\"hero-back\"],\n body:has([data-mobile-nav=\"header-back\"]) [data-mobile-nav=\"hero-back\"] {\n display: none;\n }\n}\n";
1
+ export declare const HOME_CSS = "/* ---------- phone app shell (< 768px) ---------- */\n\n@media (max-width: 767px) {\n /* --- the document never scrolls (S1.1, 2026-08-17) ---\n Real-device symptom: the workspace title bar, the session header, the\n composer and the FAB all slid with the finger \u2014 every fixed surface\n \"followed the drag\". They are not misplaced; the whole DOCUMENT was\n rubber-banding under them (they are absolute/in-flow inside the frame,\n so a document-level bounce moves them all as one).\n\n After the S2.1 box-sizing fix the document has no overflow at all\n (scrollHeight === clientHeight, measured with ?mobile-nav-inset=54), so\n this is not scrolling \u2014 it is iOS's elastic overscroll, which happens on\n an unscrollable document too, and which an inner scroller chains into as\n soon as it hits its own end.\n\n overscroll-behavior: none on the viewport kills both halves at once: the\n document itself gets no bounce, and overscroll chained up from the\n message flow / session list is absorbed without moving anything.\n overflow: hidden then hard-locks the document scroller so future content\n can never reintroduce a real scroll. Set on html AND body: the spec\n propagates the viewport's value from html, but engines have historically\n read body, and neither is a scroll container we ever want.\n\n Deliberately NOT position: fixed on body \u2014 the app shell does not need\n it, and it is the variant that strands iOS's fixed elements behind the\n on-screen keyboard. With plain overflow: hidden, iOS still pans the\n visual viewport to reveal a focused textarea, so the composer stays\n visible while typing without any visualViewport JS. */\n html,\n body {\n overflow: hidden !important;\n overscroll-behavior: none !important;\n }\n\n /* The message flow is the one scroller that regularly hits its end under a\n finger. Root-level `none` already absorbs the chain, but declaring it\n at the source keeps the guarantee if the root rule is ever weakened\n (the home list and both sheets already declare it).\n\n `none`, NOT `contain` (S4.1 fix, 2026-08-17). The two differ in exactly\n the way that bit us: `contain` stops overscroll from CHAINING OUT to the\n document but deliberately KEEPS this element's own local elastic bounce,\n while `none` suppresses that local bounce too. S1.1 only reasoned about\n the chain, so it picked `contain` \u2014 and the composer went on twitching\n upward on every drag-past-the-end while the header and FAB stayed put.\n\n Why only the composer moved: the official composer seat\n (`[class$=\"_composerSeat\"]`) is `position: sticky; bottom: 0` and lives\n INSIDE this scroller (measured 2026-08-17: seat 0,710 390x134 inside\n _scrollBody 0,131 390x713, contains() === true). A sticky box is laid out\n against its scroll container's content, so the container's own rubber-band\n drags it along. The header/FAB are absolute on the frame, outside this\n scroller entirely \u2014 which is precisely why S1.1's document-level fix\n pinned them and left this one surface behind.\n\n Deliberately NOT re-pinning the seat with position: fixed: sticky inside\n the scroller is what keeps the focused textarea visible when iOS pans the\n visual viewport for the keyboard, and fixed is the variant that strands\n it behind the keyboard (same trap as the body rule above). Kill the\n bounce, keep the sticky. */\n [data-phase] [class$=\"_scrollBody\"] {\n overscroll-behavior: none !important;\n }\n\n /* The official sidebar is no longer a drawer on a phone \u2014 the home screen\n replaced it. Hidden outright (not translated off-screen) so it cannot\n capture taps or hold layout. */\n [data-mobile-nav=\"frame\"] > :first-child {\n display: none !important;\n }\n /* The header's drawer toggle opened that sidebar; with it gone the button\n has nothing to open. (S2 owns the rest of the header layout \u2014\n styles/header.css.ts \u2014 including the Files button's replacement.) */\n [data-mobile-nav=\"toggle\"] {\n display: none !important;\n }\n\n /* --- level 1: the session list ---\n Renders inside the shell overlay layer (absolute, pointer-events: none),\n so the page re-enables pointer events for itself. The layer's containing\n block is the frame's padding box, which still starts under the status\n bar \u2014 hence the safe-area padding here. */\n [data-mobile-nav=\"home\"] {\n position: absolute;\n inset: 0;\n /* rc.3 raise (real-device report 2026-10-25 \"top strip leaking the\n conversation through\"): this shell layer mounts INSIDE the host's\n PIRFFq_overlayLayer (z-index:20, new in rc.3 \u2014 the conversation\n content itself moved under it too), so the old z:5 let host session\n content (STRONG/CODE rows measured at viewport y=4) paint over the\n shell's top bar on BOTH pages, reading as a faint fixed strip at the\n window top. z:30 puts the whole shell above everything the host\n stacks inside that layer; the shell's own transparent middle and\n pointer-events passthrough keep the conversation visible and tappable\n (verified live via console injection before landing). */\n z-index: 30;\n display: flex;\n flex-direction: column;\n pointer-events: auto;\n /* dsw-specific-sidebar-fill, not an alias bg-layer-* token (real-device\n round 2 follow-up, 2026-08-17): fetched and diffed the live theme\n CSS (both light/dark blocks in /assets/index-*.css) because computed\n values, not source-read guesses, are what actually matter here \u2014\n --dsw-alias-bg-base/-layer-1/-layer-2 all resolve to the exact same\n color in the LIGHT theme (neutral-bluish-00, i.e. plain white); they\n only diverge in dark mode.\n\n 2026-08-20: the page and the CARDS swapped tokens. This surface used\n to be --dsw-specific-sidebar-fill, which made the list page a\n different shade from the session page \u2014 measured on-device in dark\n theme, rgb(27,27,28) here against rgb(21,21,23) there, a visible step\n every time you opened a session. The page now takes bg-base, the same\n token the session page and <body> resolve to, so the two screens are\n literally the same colour; the cards took sidebar-fill in exchange, so\n the contrast that separates a row from the page is preserved \u2014 it\n just runs the other way (raised card on a base page, the more\n conventional reading of the two anyway). */\n background: var(--dsw-alias-bg-base, #ffffff);\n color: var(--dsw-alias-label-primary, inherit);\n padding-top: var(--mnav-sat);\n transform: translateX(0);\n opacity: 1;\n transition:\n transform .3s var(--ds-ease-in-out, ease-in-out),\n opacity .3s var(--ds-ease-in-out, ease-in-out);\n }\n /* Push transition: the list slides out to the left as the session takes\n the screen, and comes back from the left. It stays mounted (visibility,\n not display) so the slide has something to animate; the delayed\n visibility keeps it non-interactive the moment the transition ends. */\n [data-mobile-nav=\"home\"][data-view=\"session\"] {\n transform: translateX(-100%);\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n transition:\n transform .3s var(--ds-ease-in-out, ease-in-out),\n opacity .3s var(--ds-ease-in-out, ease-in-out),\n visibility 0s .3s;\n }\n /* The host's SESSION header must not sit on top of this list page.\n Real-device report (2026-10-22, DSH 0.2.0-rc.2, dsh-web-mobile 3.2.1,\n 360x792 phone): on the list page the top strip showed the LAST SESSION's\n header \u2014 its title plus the session's tab row \u2014 and the workspace switcher\n underneath was neither visible nor tappable, so \"switch workspace\" did\n nothing at all. Measured with elementFromPoint(150,30) on the phone\n viewport: span.wSkVaW_crumbCurrent, i.e. the host header's crumb, while\n our own workspace button sat at [46,8,208,44] underneath it.\n\n Why z-index is not the fix: this page is position:absolute; z-index:5\n inside the shell overlay layer, and the host header carries its own\n stacking context, so raising us above its z-index:30 changed nothing\n (measured: the crumb still won the hit test). The session header has to\n leave the hit tree while the list page is up \u2014 the list page draws its\n own top bar (home-top, with the workspace switcher).\n\n The gate is this page's own marker: data-view=\"session\" means a session\n is open, so the rule lapses exactly when the host header is wanted again\n (verified: entering a session restores display:grid, the back button in\n that header stays tappable, and going back hides it once more). Two\n selectors on purpose \u2014 the class suffix survives CSS-module hash renames,\n the structural one survives a rename of the class itself. */\n [data-mobile-nav=\"frame\"]:not([data-mobile-view=\"session\"]) header[class*=\"_header\"],\n [data-mobile-nav=\"frame\"]:not([data-mobile-view=\"session\"]) header:has([class*=\"_titleRow\"]),\n body:has([data-mobile-nav=\"home\"]:not([data-view=\"session\"])) [data-mobile-nav=\"frame\"] header[class*=\"_header\"],\n body:has([data-mobile-nav=\"home\"]:not([data-view=\"session\"])) [data-mobile-nav=\"frame\"] header:has([class*=\"_titleRow\"]) {\n display: none !important;\n }\n @media (prefers-reduced-motion: reduce) {\n [data-mobile-nav=\"home\"],\n [data-mobile-nav=\"home\"][data-view=\"session\"] {\n transition: none !important;\n }\n }\n\n /* Title bar: the workspace name IS the switcher. */\n [data-mobile-nav=\"home-top\"] {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n min-height: 52px;\n padding: 4px 16px 6px;\n }\n /* Site logo (real-device round 2, 2026-08-17): rendered only when\n document.head actually has a <link rel=\"icon\"> (MobileHome.tsx never\n ships a placeholder box), so this rule only ever needs to size the\n image, not reserve space for its absence. */\n [data-mobile-nav=\"home-logo\"] {\n width: 24px;\n height: 24px;\n margin-right: 6px;\n flex: none;\n border-radius: 6px;\n object-fit: contain;\n /* rc.3 fix (2026-10-25, Safari real-device report \"logo \u4E0D\u663E\u793A\"): the\n host now ships TWO media-scoped favicons and the first in DOM order\n is the DARK one \u2014 a WHITE whale (fill #fff). useSiteIconHref picks\n the first link unconditionally, so on a light page the logo arrived\n white-on-light and vanished. brightness(0) collapses ANY source (the\n old single black favicon or the new white one) to a black silhouette,\n making light mode correct for both hosts; the dark-theme rules below\n re-invert it to white. */\n filter: brightness(0);\n }\n /* Dark theme (real-device report, 2026-08-17): force a white silhouette\n regardless of the source image's own colors \u2014 brightness(0) collapses\n every opaque pixel to black first, invert(1) then flips that to white;\n transparent pixels stay transparent either way. Three dark signals,\n deliberately redundant: the pre-rc.3 body[data-ds-dark-theme] marker,\n and rc.3's resolved <html data-ds-theme-source> (\"dark\" pinned, or\n \"system\" under a dark OS \u2014 rc.3 stamps the resolved scheme on\n html[style] with no body attribute until the first theme flip). */\n body[data-ds-dark-theme] [data-mobile-nav=\"home-logo\"],\n html[data-ds-theme-source=\"dark\"] [data-mobile-nav=\"home-logo\"] {\n filter: brightness(0) invert(1);\n }\n @media (prefers-color-scheme: dark) {\n html[data-ds-theme-source=\"system\"] [data-mobile-nav=\"home-logo\"] {\n filter: brightness(0) invert(1);\n }\n }\n [data-mobile-nav=\"ws-switch\"] {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n min-height: 44px;\n max-width: 100%;\n padding: 0 6px 0 0;\n border: none;\n background: transparent;\n color: inherit;\n font-family: inherit;\n font-size: 22px;\n font-weight: 600;\n line-height: 1.2;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"ws-switch\"] > span {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n [data-mobile-nav=\"ws-switch\"] > svg {\n flex: none;\n opacity: .55;\n }\n [data-mobile-nav=\"ws-switch\"]:active {\n opacity: .6;\n }\n\n /* Session list: rounded cards, not bordered rows (real-device round 2\n follow-up, 2026-08-17 \u2014 reference: Claude Code mobile app's session\n list). `gap` on the flex column IS the inter-card spacing; no\n per-row margin bookkeeping. */\n [data-mobile-nav=\"home-list\"] {\n flex: 1 1 auto;\n min-height: 0;\n margin: 0;\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px 16px calc(var(--mnav-sab) + 96px);\n list-style: none;\n overflow-y: auto;\n overscroll-behavior: contain;\n -webkit-overflow-scrolling: touch;\n }\n /* Swipe-to-archive wrapper (redone 2026-08-25, real-device round): iOS-mail\n mechanics. The wrapper CLIPS (overflow hidden, same 22px radius as the\n card) and the archive button is parked past the right edge, sliding in\n lock-step with the card \u2014 a 1px drag reveals a 1px slice, never a\n fully-drawn button behind a gap. Clipping would eat the card's cast\n shadow, so the light-theme shadow moves up to this wrapper (identical\n values; the dark theme's INSET hairline on the card survives clipping\n untouched). pan-y keeps vertical list scrolling native; the direction\n lock in MobileHome.tsx decides horizontal-vs-vertical once per touch. */\n [data-mobile-nav=\"home-swipe\"] {\n position: relative;\n overflow: hidden;\n border-radius: 22px;\n /* The list is a flex column: without this, an overflowing list SHRINKS\n every row and the clip above slices the squashed card's bottom edge\n flat (real-device report, 2026-08-25). Rows keep natural height; the\n list scrolls. */\n flex: 0 0 auto;\n touch-action: pan-y;\n box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 4px 12px rgba(0, 0, 0, .05);\n }\n body[data-ds-dark-theme] [data-mobile-nav=\"home-swipe\"] {\n box-shadow: none;\n }\n /* Press = settle toward the page (mirrors the card's own :active swap,\n which the clip made invisible at wrapper level). */\n [data-mobile-nav=\"home-swipe\"]:has([data-mobile-nav=\"home-row\"]:active) {\n box-shadow: 0 1px 2px rgba(0, 0, 0, .05);\n }\n body[data-ds-dark-theme] [data-mobile-nav=\"home-swipe\"]:has([data-mobile-nav=\"home-row\"]:active) {\n box-shadow: none;\n }\n [data-mobile-nav=\"home-swipe-card\"] {\n position: relative;\n z-index: 1;\n transition: transform .22s var(--ds-ease-out, ease-out);\n will-change: transform;\n }\n /* Inset pill, not a full-bleed slab (real-device feedback: hard edges).\n 5px breathing room on three sides inside the 88px reveal; its own\n radius echoes the card family. It still slides in lock-step from past\n the right edge (transform driven from MobileHome.tsx). */\n [data-mobile-nav=\"home-archive\"] {\n position: absolute;\n z-index: 0;\n top: 5px;\n bottom: 5px;\n right: 5px;\n width: 78px;\n border: none;\n border-radius: 17px;\n background: var(--dsw-alias-state-error-primary, #d4494c);\n color: #fff;\n font-size: 13px;\n font-weight: 600;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n transition: transform .22s var(--ds-ease-out, ease-out);\n will-change: transform;\n }\n [data-mobile-nav=\"home-archive\"]:active {\n filter: brightness(.9);\n }\n\n [data-mobile-nav=\"home-row\"] {\n display: flex;\n align-items: center;\n gap: 12px;\n width: 100%;\n min-height: 60px;\n padding: 14px 16px;\n border: none;\n border-radius: 22px;\n /* The page's old colour (see the swap note on [data-mobile-nav=\"home\"]):\n differs from bg-base in BOTH themes, and is the token\n dsh-better-sidebar's panel already uses for a secondary surface. */\n background: var(--dsw-specific-sidebar-fill, #f5f5f5);\n color: inherit;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n /* Card edge definition (2026-08-17). Light theme only \u2014 see the dark\n override below. Two stacked shadows in the iOS idiom: a tight 1px\n contact shadow that draws the edge itself, plus a wide soft one that\n lifts the card off the page. Both are deliberately weak (.06/.05):\n the page sits on --dsw-specific-sidebar-fill and the card on\n --dsw-alias-bg-base, so there is already a slight tonal step here and\n the shadow only has to sharpen it, not carry it alone. */\n box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 4px 12px rgba(0, 0, 0, .05);\n }\n [data-mobile-nav=\"home-row\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n /* Press = settle toward the page. Dropping the wide shadow and keeping a\n tighter contact one reads as the card pushing in, which is what the\n darkening background already implies; leaving the lifted shadow under\n a pressed card is the combination that looks wrong. */\n box-shadow: 0 1px 2px rgba(0, 0, 0, .05);\n }\n /* Dark theme: a black shadow on a near-black page is invisible, so the\n edge is drawn instead of cast. A 1px hairline at --dsw-alias-border-l2\n weight, delivered as an INSET box-shadow rather than a real border so\n the card's geometry is untouched (a real 1px border would grow every\n row by 2px unless box-sizing cooperated) and so it overrides the light\n shadow through the same property instead of fighting it.\n\n Chosen over \"brighten the card face\" because in dark theme the card\n (--dsw-alias-bg-base, bluish-950) is already DARKER than the page\n (--dsw-specific-sidebar-fill, bluish-900); lightening the card past the\n page would invert the layering that the light theme establishes, while\n a hairline keeps both themes reading as \"page behind, card in front\".\n\n Theme is read from body[data-ds-dark-theme], never prefers-color-scheme:\n the app hardcodes color-scheme: light on <html> and switches themes with\n this attribute, so a media query would never fire (AGENTS.md). This\n selector also outranks the :active rule above (0,2,1 vs 0,2,0), so one\n declaration covers the pressed state too. */\n body[data-ds-dark-theme] [data-mobile-nav=\"home-row\"] {\n box-shadow: inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(255, 255, 255, .12));\n }\n [data-mobile-nav=\"home-row\"][data-current] [data-mobile-nav=\"home-row-title\"] {\n font-weight: 600;\n }\n /* Avatar: a running/warning/done session shows its existing StateDot\n (same component, same semantics as the old inline dot \u2014 just bigger\n and re-homed); otherwise the title's own first character stands in\n for it, so every row has a mark even when idle. interactive-bg-hover,\n not a bg-layer-* token: same reason as the page background above \u2014\n bg-layer-2 is IDENTICAL to the card's own bg-base in light theme\n (verified against the live theme CSS), so it would render invisible\n there. interactive-bg-hover is a translucent rgba tint rather than a\n solid layer color, so it always reads as \"a shade over the card\" in\n both themes regardless of what the card's base color resolves to. */\n [data-mobile-nav=\"home-row-avatar\"] {\n flex: none;\n display: grid;\n place-items: center;\n width: 40px;\n height: 40px;\n border-radius: 12px;\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .55));\n font-size: 16px;\n font-weight: 600;\n }\n [data-mobile-nav=\"home-row-body\"] {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n [data-mobile-nav=\"home-row-title\"] {\n font-size: 16px;\n line-height: 21px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n [data-mobile-nav=\"home-row-status\"] {\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-size: 12.5px;\n line-height: 17px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n [data-mobile-nav=\"home-row-time\"] {\n flex: none;\n align-self: flex-start;\n color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .4));\n font-size: 12.5px;\n line-height: 18px;\n }\n [data-mobile-nav=\"home-empty\"] {\n margin: 12px 16px;\n padding: 48px 24px;\n border-radius: 22px;\n /* Stands in for the rows, so it carries the rows' surface. */\n background: var(--dsw-specific-sidebar-fill, #f5f5f5);\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-size: 15px;\n text-align: center;\n }\n\n /* New-session FAB: a labeled pill (real-device round 2 follow-up), not a\n bare circle \u2014 tap starts in the shown workspace, long press picks\n one. Inverted-surface tokens (not a hardcoded accent color): the same\n pair the official git-commit button in dsh-better-sidebar uses for\n its own solid CTA. */\n [data-mobile-nav=\"home-fab\"] {\n position: absolute;\n right: 18px;\n bottom: calc(var(--mnav-sab) + 22px);\n z-index: 6;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 48px;\n padding: 0 20px 0 16px;\n border: none;\n border-radius: 999px;\n background: var(--dsw-alias-button-primary-fill, #1a1a1a);\n color: var(--dsw-alias-label-primary-inverted, #ffffff);\n font-family: inherit;\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n box-shadow: 0 6px 20px rgba(0, 0, 0, .24);\n touch-action: manipulation;\n user-select: none;\n -webkit-user-select: none;\n -webkit-touch-callout: none;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"home-fab\"]:active {\n transform: scale(.96);\n }\n\n /* Workspace sheet (switcher and long-press New Session share it). */\n [data-mobile-nav=\"home-sheet-layer\"] {\n position: absolute;\n inset: 0;\n z-index: 7;\n }\n [data-mobile-nav=\"home-sheet-mask\"] {\n position: absolute;\n inset: 0;\n background: var(--dsw-alias-bg-mask-3, rgba(0, 0, 0, .45));\n border: none;\n animation: dsh-mobile-nav-fade .18s var(--ds-ease-out, ease-in-out);\n }\n /* Docked to the screen edge, matching the composer's permission/model\n sheets (styles/composer.css.ts section 4) rather than floating.\n\n It used to sit at `bottom: calc(var(--mnav-sab) + 8px)` with all four\n corners rounded, which left a 42px strip of bare mask under it at\n sab: 34 (measured). Docking removes the strip instead of dimming it, and\n the safe area moves INSIDE the card as bottom padding \u2014 so the white\n card body runs all the way to the physical screen edge and the last row\n still clears the home indicator. Same reason the corners go top-only:\n a rounded bottom corner on a card flush with the screen edge reads as a\n rendering mistake. */\n [data-mobile-nav=\"home-sheet\"] {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n max-height: 70%;\n overflow-y: auto;\n overscroll-behavior: contain;\n box-sizing: border-box;\n padding: 6px 6px calc(var(--mnav-sab) + 14px);\n border-radius: 20px 20px 0 0;\n /* layer-2, not bg-base: in dark mode the sheet must lift off a page that\n shares bg-base, or only the shadow separates them. */\n background: var(--dsw-alias-bg-layer-2, #ffffff);\n box-shadow: 0 -8px 32px rgba(0, 0, 0, .28);\n animation: dsh-mobile-nav-sheet-up .22s var(--ds-ease-out, ease-in-out);\n }\n @media (prefers-reduced-motion: reduce) {\n [data-mobile-nav=\"home-sheet-mask\"],\n [data-mobile-nav=\"home-sheet\"] {\n animation: none !important;\n }\n }\n [data-mobile-nav=\"home-sheet-title\"] {\n padding: 10px 12px 6px;\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-size: 13px;\n }\n [data-mobile-nav=\"home-sheet-item\"] {\n display: flex;\n align-items: center;\n width: 100%;\n min-height: 48px;\n padding: 0 12px;\n border: none;\n border-radius: 12px;\n background: transparent;\n color: inherit;\n font-family: inherit;\n font-size: 16px;\n text-align: left;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"home-sheet-item\"][data-selected] {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n font-weight: 600;\n }\n [data-mobile-nav=\"home-sheet-item\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n }\n\n /* --- hero fallback back button ---\n The hero (new blank session) page renders no conversation.session.header,\n so the header-slot back button (S2) does not exist there. This floating\n fallback covers that page and hides itself as soon as the real header\n back mounts. */\n [data-mobile-nav=\"hero-back\"] {\n position: absolute;\n top: calc(var(--mnav-sat) + 8px);\n left: 8px;\n z-index: 6;\n width: 44px;\n height: 44px;\n display: grid;\n place-items: center;\n border: none;\n border-radius: 14px;\n background: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n pointer-events: auto;\n }\n [data-mobile-nav=\"hero-back\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n }\n [data-mobile-nav=\"frame\"]:has([data-mobile-nav=\"header-back\"]) ~ [data-mobile-nav=\"hero-back\"],\n body:has([data-mobile-nav=\"header-back\"]) [data-mobile-nav=\"hero-back\"] {\n display: none;\n }\n}\n";
2
2
  //# sourceMappingURL=home.css.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"home.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/home.css.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,QAAQ,glyBAokBpB,CAAA"}
1
+ {"version":3,"file":"home.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/home.css.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,QAAQ,q7zBAmlBpB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-zen-remote",
3
- "version": "2.2.6",
3
+ "version": "2.2.8",
4
4
  "description": "一个插件、两种角色:主服务端把 DeepSeek Harness 变成手机能安全访问的 PWA(移动端界面、配对码网关、Web Push),并把选定会话共享给已配对的桌面端;子客户端在另一台 DSH 桌面端的侧边栏里远程使用这些会话。One DSH plugin, two roles: the host turns DeepSeek Harness into a phone-reachable PWA (mobile UI, pairing-code gateway behind your own reverse proxy, Web Push) and shares chosen sessions to paired desktops; the client uses those sessions remotely from another DSH desktop app's sidebar.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,6 +11,7 @@
11
11
  //
12
12
  // Run: node scripts/check-attach-upload.mjs (needs Node >= 23.6 type stripping)
13
13
  import assert from 'node:assert/strict'
14
+ import fs from 'node:fs'
14
15
  import {
15
16
  UPLOAD_DIR,
16
17
  UPLOAD_ROUTE,
@@ -133,4 +134,45 @@ assert.ok(long.startsWith('quarterly'), long)
133
134
  assert.ok(long.endsWith('.pdf'), long)
134
135
  assert.ok(long.includes('…'))
135
136
 
137
+ // ---- static regression: the #31 attach restore ------------------------------
138
+ // The host-attach stand-down is gone for good (#31: DSH 0.2.x kept the hidden
139
+ // <input type="file"> after moving the paperclip into the "+" menu, so the
140
+ // probe matched everywhere and the phone lost its only attach affordance).
141
+ // The probe file must stay deleted, the two components must not reference it,
142
+ // and composer.css.ts must keep hiding the second official _add paperclip
143
+ // that DSH ≤ 0.1.5 still renders below 768px.
144
+ assert.equal(
145
+ fs.existsSync(new URL('../src/client/host-attach.ts', import.meta.url)),
146
+ false,
147
+ 'src/client/host-attach.ts must stay deleted (the removed stand-down probe)',
148
+ )
149
+ for (const rel of ['../src/client/MobileAttachButton.tsx', '../src/client/MobileAttachChips.tsx']) {
150
+ const source = fs.readFileSync(new URL(rel, import.meta.url), 'utf8')
151
+ assert.ok(
152
+ !source.includes('useOwnAttachUi') && !source.includes('host-attach'),
153
+ `${rel} must not reference the removed host-attach stand-down`,
154
+ )
155
+ }
156
+ // The hide rule must live in the PHONE media block only — desktop (>= 768px)
157
+ // keeps the official paperclip untouched, so match inside the 767px block.
158
+ const composer = fs.readFileSync(new URL('../src/client/styles/composer.css.ts', import.meta.url), 'utf8')
159
+ const phoneStart = composer.indexOf('@media (max-width: 767px)')
160
+ const phoneEnd = composer.indexOf('@media (min-width: 768px)')
161
+ assert.ok(
162
+ phoneStart !== -1 && phoneEnd !== -1 && phoneStart < phoneEnd,
163
+ 'composer.css.ts phone/desktop media blocks not found in expected order',
164
+ )
165
+ assert.match(
166
+ composer.slice(phoneStart, phoneEnd),
167
+ /\$\{ROW\} > \[class\$\="_tools"\] > button\[class\$\="_add"\]:nth-of-type\(2\)\s*\{[^}]*display: none !important/,
168
+ 'composer.css.ts must hide the second official _add paperclip below 768px (#31)',
169
+ )
170
+ // The subagent gate must stay: the host disables its own file input while a
171
+ // subagent session is open, so ours stands down with it (#31 review).
172
+ assert.ok(
173
+ fs.readFileSync(new URL('../src/client/MobileAttachButton.tsx', import.meta.url), 'utf8')
174
+ .includes("row?.origin === 'subagent'"),
175
+ 'MobileAttachButton must gate the paperclip off on subagent sessions',
176
+ )
177
+
136
178
  console.log('ATTACH UPLOAD CHECK OK')