.root { position: relative; /* positioning context for slot-owned absolute chrome */ display: flex; flex-direction: column; height: 100%; min-width: 0; background: var(--dsw-alias-bg-base); } /* 76px with its rule: the height of the Sidebar's tab strip and header row (ui-dockkit `.tabStrip` 38px + the pane header's 38px), so the two rules meet at the column edge. The rows add up to it — 10px top inset, the 30px title row, the tab strip's 10px margin, and a 25px view row. When the views and the More launcher do not fit on one row, the row wraps and the header grows past 76px. */ .header { display: grid; grid-template-columns: auto minmax(0, 1fr); flex: none; box-sizing: border-box; min-height: 76px; padding: 10px 28px 0 20px; border-bottom: 0.5px solid var(--dsw-alias-border-l3); } .header:where(:not(:has(.tabs))) { min-height: 0; padding-bottom: 10px; } /* Blank Sessions retain navigation controls without conversation title or tabs. */ .headerBlank { min-height: 0; padding-bottom: 0; border-bottom: none; } .headerBlank .headerCorner { margin-left: auto; } /* Only the macOS titlebar needs reserved drag space before a Session exists. */ :global(html:not([data-platform='darwin'])) .headerSessionless { padding-top: 0; } :global(html:not([data-platform='darwin'])) .headerSessionless .titleRow { min-height: 0; } /* macOS hiddenInset titlebar: the header block owns the window drag over its own box — title row plus the view tab strip, so the draggable geometry is the header's geometry and no frame band has to match its height. The header marks itself `data-window-drag` (ConversationHeader); below it the title-row clusters subtract their whole boxes, and individual controls are covered by the base.css interactive-element rule. A covering overlay lives beside #root (ui-settings-general), which puts it after this row in document order. */ :global([data-platform='darwin']) .headerLeading, :global([data-platform='darwin']) .headerActions, :global([data-platform='darwin']) .headerUtilities, :global([data-platform='darwin']) .headerCorner { -webkit-app-region: no-drag; } .titleRow { grid-column: 2; /* Anonymous inline-size container (CSS modules hash container-names per module, so occupants match the nearest container): header occupants such as the preset label and slot actions shed their text at row-width breakpoints. The minmax(0, 1fr) column sizes the row, so inline-size containment cannot collapse it. */ container-type: inline-size; display: flex; align-items: center; gap: 0; min-height: 30px; /* Clears the frame's window-chrome band (traffic lights + shell.leading controls, ui-layout AppFrame) when it is published; the header already pads 20px of it. Zero everywhere else (the fallback makes the max negative). */ padding-inline-start: max(0px, calc(var(--dsh-frame-leading-clearance, 0px) - 20px)); } /* Header actions do not shrink, so on a narrow row they overflow the cluster. The cluster clips them, hit testing included, so the next control keeps every click. The trailing padding spans the gap before that control and holds the fade: content that fits ends before it, and only overflowing content reaches it. The 4px block and start padding, offset by negative margins, keeps focus rings inside the clip. `clip` rather than `hidden` keeps the cluster from becoming a scroll container that focus could scroll. */ .titleCluster { display: flex; flex: 1; align-items: center; gap: 10px; min-width: 0; --title-cluster-fade: 20px; margin: -4px calc(-1 * var(--title-cluster-fade)) -4px -4px; padding: 4px var(--title-cluster-fade) 4px 4px; overflow: clip; mask-image: linear-gradient(to left, transparent calc(0.3 * var(--title-cluster-fade)), #000 var(--title-cluster-fade)); } /* Without utilities the corner's 8px margin is the gap the fade spans. */ .titleCluster:has(+ .headerUtilities:empty) { --title-cluster-fade: 8px; } /* The auto grid column collapses with an empty or CSS-hidden occupant. The occupant owns its trailing gap so hidden controls reserve no space. */ .headerLeading { grid-column: 1; grid-row: 1; display: flex; flex: none; align-items: center; gap: 8px; } .crumbs { display: flex; align-items: center; gap: 4px; min-width: 0; overflow: hidden; white-space: nowrap; } .crumbSeg { display: inline-flex; align-items: center; gap: 4px; min-width: 0; } .crumbSep { /* figma: "/" separators are 14px caption gray (75:7903), one tint lighter than crumb text. */ color: var(--dsw-alias-label-caption); font-size: 14px; line-height: 20px; } .crumb { display: inline-block; max-width: 220px; overflow: hidden; padding: 4px 8px; border: none; border-radius: var(--dsw-radius-md); background: transparent; font-size: 14px; line-height: 20px; color: var(--dsw-alias-label-tertiary); text-overflow: ellipsis; white-space: nowrap; cursor: pointer; } .crumbSubagent { font-size: 12px; line-height: 18px; } @media (hover: hover) { button.crumb:hover { background: var(--dsw-alias-interactive-bg-hover); } } .crumbCurrent { font-weight: 500; color: var(--dsw-alias-label-primary); cursor: default; } .headerActions { display: flex; flex: none; align-items: center; gap: 8px; } .headerUtilities { display: flex; flex: none; align-items: center; gap: 8px; margin-left: 20px; } .headerUtilities:empty { display: none; } /* The far-right corner seat reaches 16px into the header's 28px right padding, so its control sits past the utilities' edge; it is laid out only while its occupant renders something. Its left margin equals the utilities' 8px gap so the header's trailing controls space evenly. */ .headerCorner { display: flex; flex: none; align-items: center; margin-left: 8px; margin-right: -16px; } .headerCorner:empty { display: none; } /* The tab row sits above the header paint so the active bar covers its rule. */ .tabs { grid-column: 1 / -1; position: relative; z-index: 1; display: flex; gap: 16px 20px; width: 100%; min-width: 0; box-sizing: border-box; align-items: flex-start; flex-wrap: wrap; margin-top: 10px; padding-left: 8px; } @media (max-width: 767px) { :global(html:not([data-platform])) .tabs { padding-left: 0; } } .viewTabs { display: flex; flex-wrap: wrap; gap: 32px; min-width: 0; min-height: 25px; } /* The 16px text line retains 9px below it. The press target extends 10px above the text into the strip's top margin and 10px to each side, matching the inline padding of the Sidebar tab chips (ui-dockkit `.tab`); negative margins keep the layout box at its 25px row and text width. The active bar spans the text and reaches 1px past the tab box to sit over the header rule. */ .tab { --dsh-tab-hit-inline: 10px; position: relative; box-sizing: border-box; min-height: 35px; margin: -10px calc(-1 * var(--dsh-tab-hit-inline)) 0; padding: 10px var(--dsh-tab-hit-inline) 9px; border: none; background: transparent; font-size: 13px; line-height: 16px; font-weight: 500; color: var(--dsw-alias-label-tertiary); cursor: pointer; } .tab::after { content: ''; position: absolute; right: var(--dsh-tab-hit-inline); bottom: -1px; left: var(--dsh-tab-hit-inline); height: 2px; border-radius: 2px; background: transparent; } /* Selected tab is blue, not ink: brand-primary resolves to neutral black in this token sheet, so the selected state rides the business blue — the nearest semantic token that stays blue in both themes. */ .tabActive { color: var(--dsw-alias-state-business-primary); } .tabActive::after { background: var(--dsw-alias-state-business-primary); } /* The More launcher centers its 22px box on the tab's 16px text line. */ .viewsMoreAnchor { flex: none; } .viewsMore { width: 22px; height: 22px; margin-top: -3px; padding: 0; color: var(--dsw-alias-label-secondary); } .viewsMoreActive { color: var(--dsw-alias-state-business-primary); } .tabLabel { display: block; max-width: 88px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .viewsMore[aria-expanded='true'] { background: var(--dsw-alias-interactive-bg-hover); } .viewsMoreMenu { width: 288px; max-width: calc(100vw - 24px); } .viewArea { display: flex; flex: 1; flex-direction: column; min-height: 0; } /* Width handles: 10px col-resize strips beside the transcript, absolute in .body (NOT the scrollport: an absolute strip there would extend the scrollable range; NOT .root: a strip from the column top would paint its glow through the header's transparent background). The inner edge sits 24px outside the content column and the strip extends 10px outward; the outer edge is clamped to keep a 24px safe zone against the column edges (sidebar side and scrollbar side stay drag-free). When the margin cannot fit inset + strip + safe zone the computed width goes negative and the strip resolves to zero — no mispositioned hit area on narrow columns. Known half-scrollbar (~4px) centering offset vs the gutter-reserved content box is accepted: it consumes part of the 10px strip but remains within it. */ .widthHandle { position: absolute; top: 0; bottom: 0; /* Concrete overflow content establishes z-index 1, while the sticky composer remains above both layers. */ z-index: 0; width: min( 10px, calc((100% - var(--dsh-chat-content-width)) / 2 - 24px - 24px) ); cursor: col-resize; } .widthHandle[data-side='left'] { right: calc(50% + var(--dsh-chat-content-width) / 2 + 24px); } .widthHandle[data-side='right'] { left: calc(50% + var(--dsh-chat-content-width) / 2 + 24px); } /* The glow: a short 2px bar following the captured pointer's Y, centered in the strip (29px from the content column), not a full-height rule — a 16px solid core centered on the pointer (published during drag as --dsh-width-handle-pointer-y), fading to transparent over 28px each side (72px total). A solid CORE, not a single solid stop: a lone peak washes out over the fade. The resting scrollbar tint keeps this secondary affordance quiet in both themes. Before the first drag move the fallback centers the glow at 50% height; hover leaves its position unchanged. */ .widthHandle::after { content: ''; position: absolute; top: 0; bottom: 0; width: 2px; border-radius: 2px; background: linear-gradient( to bottom, transparent calc(var(--dsh-width-handle-pointer-y, 50%) - 36px), var(--dsw-alias-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) - 8px), var(--dsw-alias-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) + 8px), transparent calc(var(--dsh-width-handle-pointer-y, 50%) + 36px) ); opacity: 0; pointer-events: none; } .widthHandle[data-side='left']::after { right: 4px; } .widthHandle[data-side='right']::after { left: 4px; } .widthHandle[data-dragging]::after { opacity: 1; } @media (hover: hover) { .widthHandle:hover::after { opacity: 1; } } /* A drag starts above the composer band. Pointer capture keeps it active when crossing the footer; lift only the active handle so its indicator remains visible without making the resting footer gutter draggable. */ .widthHandle[data-dragging] { z-index: 8; } /* Views that elect a composer overlay (trajectory etc.) own their full-bleed scrollers; the strips would sit over their content and steal clicks. */ .root:has([data-conversation-composer-overlay]) .widthHandle { display: none; } /* Composer context stack (Figma 9:937): standalone dock cards share one rhythm above the input card. */ .composerStack { /* Horizontal geometry (card width, clearance, dock inset) rides the shared .root variables above so takeover siblings match the stack. */ --dsh-composer-stack-gap: 6px; display: flex; flex-direction: column; gap: var(--dsh-composer-stack-gap); } /* Common seat for the composer chain (fallback + elected overlay siblings). */ .composerSeat { display: flex; flex: none; flex-direction: column; /* One cap for every scrolling text region a composer seat can hold: the InputBar draft (figma Input 75:8208 max 14 lines × 24px line) and the takeover panels' bodies top out at the same height, so electing a takeover never grows the footer past the card it replaces. Declared on the seat because it is the chain's only shared ancestor — fallback and elected overlay are siblings — and custom properties inherit down to whichever entry is mounted. */ --dsh-composer-text-max-height: 336px; } /* Active phase: header is ordinary column chrome above the scrollport (not sticky). The scroll body holds the transcript and the sticky composer seat so wheel over the footer moves the flow. */ .root[data-phase='active'] { overflow: hidden; } .root[data-phase='active'] .header { flex: none; } /* Band below the header: the scrollport plus the width handles. Anchoring the handles here keeps their full-height strips and glow under the header's bottom edge at every header height. */ .body { position: relative; display: flex; flex: 1; flex-direction: column; min-height: 0; /* Shared width axis for the transcript, dock cards, composer, and takeover cards. Main width controls publish the live body width and optional user preference; embedded occurrences override this adaptive axis below. */ --dsh-chat-content-width: var( --dsh-chat-user-width, clamp(680px, calc(var(--dsh-conversation-column-width, 0px) * 0.64), 920px) ); --dsh-composer-card-max-width: calc(var(--dsh-chat-content-width) + 32px); --dsh-composer-side-clearance: 16px; --dsh-composer-dock-inset: 8px; } .embeddedBody { overflow: hidden; --dsh-chat-content-width: min(calc(100% - 32px), 920px); --dsh-composer-card-max-width: min(calc(100% - 16px), 952px); --dsh-composer-side-clearance: 8px; --dsh-composer-dock-inset: 8px; } .scrollBody { display: flex; flex: 1; flex-direction: column; min-height: 0; /* The product-wide 2px scrollbar offset (ui-workspace WorkspaceBrowser `.list`, ui-sidebar-files FilesBody `.body`): the bar sits 2px clear of the column's edge instead of flush against it. */ margin-right: 2px; overflow-y: auto; /* Reserved unconditionally: the composer seat rides this box's content box in Chat and its padding box under a view's composer overlay, so an `auto` gutter moves the input card sideways by the bar's width whenever the two differ ([decision](../../../../../../.agents/notes/archived/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)). */ scrollbar-gutter: stable; } /* The bar also stops 2px short of the scroller's ends, matching its 2px edge offset. WebKit-only: the Firefox path has no track to inset. */ .scrollBody::-webkit-scrollbar-track { margin: 2px; } .root[data-phase='active'] .viewArea, .embeddedBody[data-content-phase='active'] .viewArea { flex: 1 0 auto; min-height: auto; } .root[data-phase='active'] .composerSeat, .embeddedBody[data-content-phase='active'] .composerSeat { position: sticky; bottom: 0; /* Above markdown CodeBlock sticky banners (z-index 6) so the footer never paints under a sticking code header while scrolling. The pinned compaction header (MessageItem.module.css, ui-chat) and the turn-navigation rail slot (TurnNavigator.module.css, ui-chat) also take 7 to clear the same banners. Among elements of equal rank the later one in DOM order wins, so the composer stays above the pinned header on the extreme small-viewport overlap and the pinned header stays above the rail. */ z-index: 7; /* Input mask (figma 1205:27463): transcript fades out under a FIXED 36px band at the seat's top (the figma 24% of the resting ~150px composer), solid below — px stops, not %, so a growing draft only widens the solid region and the fade band never stretches. The 0px stop is bg-base at zero alpha (not white, which the figma export hardcodes) so both themes fade from their own base. */ background: linear-gradient( 180deg, color-mix(in srgb, var(--dsw-alias-bg-base) 0%, transparent) 0px, var(--dsw-alias-bg-base) 36px ); } /* An open @/slash menu (data-trigger-menu, ui-input-trigger) renders inside the input card, so the seat's stacking context caps its z-index; lift the seat above the back-to-bottom control (z-index 8, ui-chat ChatView) while the menu is open. The steady state stays 7 so the control keeps painting over the card. */ .root[data-phase='active'] .composerSeat:has([data-trigger-menu]) { z-index: 9; } /* Views may opt into a composer overlay while ConversationRoot retains ownership of the seat geometry and its active-phase precedence. */ .scrollBody:has([data-conversation-composer-overlay]) { position: relative; /* A clipping box nothing scrolls out of, stated as a scroll container on both axes rather than `overflow: hidden`: a single-axis scroller computes the other axis to `auto`, and the seat's absolute positioning below needs the box to stay a containing block in every engine. */ overflow-x: hidden; overflow-y: auto; /* The view owns its own scrollers (the trajectory ledger scrolls inside the view), so this box never scrolls here; reserving a gutter would only narrow the view's content by the bar's width. Chat keeps the reservation on the same element, so the seat below compensates with the bar's width ([decision](../../../../../../.agents/notes/archived/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)). */ scrollbar-gutter: auto; } .scrollBody:has([data-conversation-composer-overlay]) > :global([data-slot='conversation.session']) > .viewArea { flex: 1 1 0; min-height: 0; overflow: hidden; } .scrollBody:has([data-conversation-composer-overlay]) > .composerSeat { position: absolute; /* Chat's seat rides this box's content box, which its own scrolling shortens by the bar's width; the overlay seat is against the padding box, so it must give back the same width to keep the input card in place across tabs. Reads ui-theme's --dsh-scrollbar-width, which mirrors the WebKit bar's layout width. Chat reserves the engine-resolved width, so on an engine where the resolved width differs from this fixed 8px the card drifts by half the difference — accepted residual risk, recorded in ([decision](../../../../../../.agents/notes/archived/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.md)). */ right: var(--dsh-scrollbar-width); bottom: 0; left: 0; } /* Hero phase: the composer stack (hero chrome + workspace row + card) is flex-centered in the column; composer phase docks it at the bottom. Flex, NOT absolute+transform: a transform would make this box the containing block for position:fixed descendants (pickers/modals), shrinking them. */ .composerHero { align-self: center; /* figma 75:8208 drew 12 between all three rows; the workspace row now sits 8 above the card (its margin-top restores 12 under the hero chrome). */ gap: 8px; /* Foot inside the centered box floats the stack a bit above true center. */ padding-bottom: 32px; /* Card cap + both clearances: the hero input card lands at exactly the same width as the docked composer at every viewport. */ width: min(calc(var(--dsh-composer-card-max-width) + 2 * var(--dsh-composer-side-clearance)), 100%); z-index: 1; } .heroWorkspaceRow { display: flex; align-items: center; gap: 2px; min-width: 0; margin-top: 4px; /* figma drew px 8; left nudged +12 so the chip's folder glyph lines up closer to the card's inner controls below; right keeps the trailing preset seat off the card edge. */ padding: 0 16px 0 20px; } /* Plugin options share the row, pushed to its right edge. */ .heroOptions { display: flex; flex: none; align-items: center; gap: 2px; min-width: 0; margin-left: auto; } .heroOptions:empty { display: none; } /* Hero: the composer sits inside the session scroll body; center there so the tree seat matches active (sticky footer) without a Root remount. */ .root[data-phase='hero'] .scrollBody, .embeddedBody[data-content-phase='hero'] .scrollBody { justify-content: center; overflow-y: auto; } /* Settling (session replaying, hero/docked unknown): keep the composer seat mounted but invisible so no wrong layout flashes before the phase lands. */ .root[data-phase='settling'] .composerSeat, .embeddedBody[data-content-phase='settling'] .composerSeat { visibility: hidden; }