/* ========================================================================== Doormile AI — Operations Copilot -------------------------------------------------------------------------- Styling for the right-side slide-over assistant. Follows the same convention as OrdersRedesign.css / Dispatch.css (a page-scoped stylesheet imported by its own component) rather than inline styles, so the panel's hover / focus / media-query / reduced-motion states are all expressible. Every value below resolves to a design token (`--color-*`, `--spacing-*`, `--radius-*`) or to a local `--dai-*` token defined once here. The local tokens exist because the AI surface needs a few values the shared DT set doesn't carry (the AI accent, the scrim, the panel elevation). ========================================================================== */ /* --------------------------------------------------------------------------- Dock geometry. These three live on :root, not on .dai-root, because the LAYOUT has to read them: the assistant is docked into the page rather than floated over it, so the app's content container shifts by exactly the panel's width and animates on the same curve. A token only .dai-root can see cannot do that. All three are --dai- prefixed. :root is shared with Dispatch.css and globalPolish.css, and an unprefixed name there is decided by bundle order. --------------------------------------------------------------------------- */ :root { /* 20vw, NOT 20%. The two consumers resolve a percentage against different boxes: the panel is position:fixed, so `width: 20%` is 20% of the VIEWPORT, while `padding-right: 20%` on the content container is 20% of ITS containing block — which is narrower by the side nav. The page would slide underneath the panel by the difference. A viewport unit resolves the same for both. max() rather than a separate min-width for the same reason: a min-width that only the panel knows about would reintroduce the mismatch on narrow screens. The floor matters more at 20vw than it did at 30vw: 20vw is 256px on a 1280px laptop, which is too narrow for the composer and a message thread. 320px is the floor, so below 1600px the panel stops shrinking and holds — and the page keeps the rest. On a 1920px monitor 20vw is 384px. Narrowed from 30vw on request. It is the reason the Deliveries tab strip started overflowing, so the extra 10vw goes back to the page. */ /* Default = the NORMAL step. AIPanel overwrites this property on the document element when the operator toggles the width, so the panel and the page's reserved strip always read the same value. */ /* clamp, not max: the cap is what makes this work on a large monitor. With `max(320px, 20vw)` the panel just kept growing — 20vw is 512px at 2560px and 768px at 4K, which is a chat column wider than most documents, while the board beside it gets squeezed for no benefit. A conversation does not read better past ~460px. So three regimes, one expression: < 1600px floor at 320px (20vw would be 256px on a 1280 laptop — too narrow for the composer and the thread) 1600-2300 20vw (scales with the screen, as asked) > 2300px cap at 460px (the page keeps everything above this) */ --dai-dock-width: clamp(320px, 20vw, 460px); --dai-duration: 240ms; --dai-ease: cubic-bezier(0.16, 1, 0.3, 1); } .dai-root { /* Brand accent — resolves to the app's accent token, which is black (root CLAUDE.md §6.2: "the brand is black", one accent only). Send button, active states and the trigger all read from this single variable, so switching the assistant to Doormile red is a one-line change here rather than a hunt through the components. */ --dai-accent: var(--color-accent, #0f172a); --dai-accent-contrast: #ffffff; /* Focus ring, tinted with the accent rather than a flat grey wash. */ --dai-accent-ring: color-mix(in srgb, var(--dai-accent) 14%, transparent); /* AI identity accent — used ONLY on the spark/orb marks so the assistant reads as an AI surface without turning the panel into a purple product. */ --dai-ai-from: #6366f1; --dai-ai-to: #8b5cf6; --dai-ai-glow: rgba(99, 102, 241, 0.18); --dai-text: #0f172a; --dai-text-secondary: #64748b; --dai-text-muted: #94a3b8; --dai-surface: #ffffff; --dai-surface-alt: #f8fafc; --dai-surface-hover: #f1f5f9; --dai-border: rgba(15, 23, 42, 0.08); --dai-border-strong: rgba(15, 23, 42, 0.14); --dai-scrim: rgba(15, 23, 42, 0.08); --dai-shadow: 0 8px 30px rgba(15, 23, 42, 0.1); --dai-live: #10b981; /* --dai-panel-width was declared here and at the 1279px breakpoint, and read nowhere: the panel and the page's reserved strip both size from --dai-dock-width. Removed so nobody tunes it expecting the panel to change. */ --dai-inset: 12px; --dai-duration: 240ms; --dai-ease: cubic-bezier(0.16, 1, 0.3, 1); } /* -------------------------------------------------------------------------- Scrim — deliberately light. The dashboard underneath must stay readable; this is a layering cue, not a modal blackout. -------------------------------------------------------------------------- */ /* The scrim exists for the MOBILE presentation only (see the media query at the end of this block). On desktop the panel is docked into the layout and dimming the page behind it would be a lie — the page is still live, still scrollable, and still the thing the operator is working on. Dimming is also the single strongest cue that something is modal, which is exactly the impression this panel should not give. */ .dai-scrim { display: none; position: fixed; inset: 0; z-index: 1300; background: var(--dai-scrim); opacity: 0; transition: opacity var(--dai-duration) var(--dai-ease); } .dai-scrim[data-open='true'] { opacity: 1; } /* -------------------------------------------------------------------------- Panel -------------------------------------------------------------------------- */ /* DOCKED, not floating. It used to be an inset card: 12px off every edge, fully rounded, drop shadowed, over a scrim — every cue of a popup. It now sits flush in the right 30% of the workspace, starting under the app header and running to the bottom of the window, with a single hairline on its left edge. The page does not move underneath it; it makes room for it. `top` reads Astryx's own measured header height so the dock lines up with the bottom of the top nav rather than guessing a number. No radius and no drop shadow: both are what make a surface read as floating ABOVE the page. A soft shadow is kept only as a left-edge falloff so the seam has depth without the panel detaching. */ .dai-panel { position: fixed; /* Inset card geometry matching the reference design: Floating cleanly off the top nav, bottom edge, and right edge with rounded corners. */ top: calc(var(--dai-dock-top, 57px) + 14px); right: 16px; bottom: 14px; z-index: 1200; width: calc(var(--dai-dock-width) - 24px); display: flex; flex-direction: column; min-height: 0; overflow: hidden; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 20px; box-shadow: 0 4px 24px -2px rgba(15, 23, 42, 0.08), 0 1px 3px rgba(15, 23, 42, 0.04); transform: translateX(calc(100% + 32px)); visibility: hidden; transition: transform var(--dai-duration) var(--dai-ease), visibility 0s linear var(--dai-duration); } .dai-panel[data-open='true'] { transform: translateX(0); visibility: visible; transition: transform var(--dai-duration) var(--dai-ease), visibility 0s; } /* ---- The page makes room ------------------------------------------------- `.dai-docked` is set on while the panel is open. Padding rather than width/margin: the content container is a flex child inside AppShell's own height:fill chain, and changing its width there fights that chain, whereas padding leaves the box model alone and simply reserves the strip. The transition sits on the container unconditionally so the page slides back when the panel closes too — a rule that only exists while `.dai-docked` is applied cannot animate its own removal. */ .dai-page, .astryx-layout-content { transition: padding-right var(--dai-duration) var(--dai-ease); } body.dai-docked .dai-page, body.dai-docked .astryx-layout-content { /* Reserves the dock width plus a generous 20px gutter so the page content (tables, cards) never collides with the sidebar and stays distinct. */ padding-right: calc(var(--dai-dock-width) + 16px); } /* ---- Mobile: there is no 30% worth having ------------------------------- 30% of a phone is ~120px. Below the breakpoint the panel becomes what it used to be everywhere — a full-width overlay with a scrim — and the page stops reserving a strip it cannot afford. */ @media (max-width: 900px) { .dai-panel { top: 0; right: 0; bottom: 0; width: 100%; z-index: 1301; border: none; border-radius: 0; box-shadow: var(--dai-shadow); transform: translateX(100%); } .dai-scrim { display: block; } .dai-scrim[data-open='true'] { opacity: 1; } body.dai-docked .dai-page, body.dai-docked .astryx-layout-content { padding-right: 0; } } .dai-panel:focus { outline: none; } /* -------------------------------------------------------------------------- Header -------------------------------------------------------------------------- */ .dai-root .dai-header { flex: 0 0 auto; padding: 14px 16px; border-bottom: 1px solid #f1f5f9; background: #ffffff; } .dai-root .dai-title { font-size: 16px; font-weight: 700; line-height: 1.2; letter-spacing: -0.01em; color: #0f172a; } .dai-root .dai-subtitle { font-size: 12px; line-height: 1.3; color: #64748b; font-weight: 500; } /* The AI mark. A soft gradient orb — not a robot face. */ .dai-root .dai-spark { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; border-radius: 10px; background: #fff1f2; border: 1px solid #ffe4e6; overflow: hidden; } /* The mark's own canvas is only 66.8% content — a third of every edge is transparent padding, measured off the PNG's alpha bounding box (x and y both 170..853 of 1024, i.e. perfectly centred). Drawing it at 100% therefore rendered a D two-thirds the size the box implied, which is exactly why it read as too small. 150% cancels that padding so the D fills its box edge to edge; the overflow:hidden above clips only transparent pixels. */ .dai-root .dai-spark img { width: 150%; height: 150%; max-width: none; object-fit: contain; display: block; } .dai-root .dai-spark[data-size='sm'] { width: 30px; height: 30px; } .dai-root .dai-spark[data-size='md'] { width: 40px; height: 40px; } .dai-root .dai-spark[data-size='lg'] { width: 56px; height: 56px; } /* Live indicator — subtle, not a large pill. */ .dai-root .dai-live { display: inline-flex; align-items: center; gap: 5px; padding: 3px 8px; border-radius: 999px; font-size: 11px; font-weight: 550; color: #047857; background: rgba(16, 185, 129, 0.08); white-space: nowrap; } .dai-root .dai-live-dot { width: 6px; height: 6px; border-radius: 999px; background: var(--dai-live); animation: dai-pulse 2.4s ease-in-out infinite; } @keyframes dai-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.55; transform: scale(0.85); } } /* Page-context strip — "Orders · Today · All locations" */ .dai-root .dai-context { display: none; } /* -------------------------------------------------------------------------- Welcome state -------------------------------------------------------------------------- */ .dai-root .dai-welcome { padding: 22px 14px 8px; } .dai-root .dai-welcome-greeting { font-size: 17px; font-weight: 650; letter-spacing: -0.01em; color: var(--dai-text); } .dai-root .dai-welcome-lead { font-size: 13.5px; line-height: 1.5; color: var(--dai-text-secondary); } .dai-root .dai-welcome-note { font-size: 12px; line-height: 1.5; color: var(--dai-text-muted); } .dai-root .dai-section-label { font-size: 11px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--dai-text-muted); } /* -------------------------------------------------------------------------- Suggestion cards -------------------------------------------------------------------------- */ /* The suggestions strip sits between the conversation and the composer, so it needs the composer's horizontal rhythm and a hairline to separate it from the thread scrolling above it. */ .dai-root .dai-suggestions-bar { /* Shrinkable, with a hard cap. At the normal 25% width the full twenty chips still stack to several rows. As `flex: 0 0 auto` that block refuses to shrink, so on a short laptop it pushes the composer off the bottom of the panel — the input disappears and the assistant cannot be used at all. The cap is a safety valve, not a layout choice: it only engages when the strip would otherwise cost more than a third of the panel, and it never applies once a thread exists, because the strip is four chips by then. */ flex: 0 1 auto; min-height: 0; max-height: 34vh; overflow-y: auto; padding: 10px 14px 8px; border-top: 1px solid var(--dai-border); background: var(--dai-surface); } /* Follow-up mode: a handful of chips, so the strip is FIXED — it never shrinks and never scrolls. The cap and `flex-shrink: 1` above exist for the cold panel's twenty chips. Once a conversation starts the strip is three or four, and leaving it shrinkable meant a long thread squeezed it: the flex row gave the space to the conversation, `overflow-y: auto` quietly hid the remainder, and the follow-ups the operator was meant to act on disappeared below a fold nobody could see. Few chips must always be fully visible. */ .dai-root .dai-suggestions-bar[data-compact='true'] { flex: 0 0 auto; max-height: none; overflow: visible; } /* ---- Suggested questions: chips ------------------------------------------ They wrap, they never scroll. A horizontal scroller can always leave a chip half-visible at the edge, and a half-visible button is a bug no amount of fade masking fixes. */ .dai-root .dai-suggestions-bar .dai-section-label { display: none; } .dai-root .dai-suggestions { /* Row + wrap, which is what the comment above always claimed and what the component asks for — SuggestionChips renders an HStack with wrap="wrap". This rule said `column`, overriding it, so four chips became four full rows and the strip ate a third of the panel before a single question had been asked. Two short chips now share a row and only long ones take one to themselves. */ width: 100%; display: flex; flex-direction: row; flex-wrap: wrap; gap: 8px; align-items: flex-start; align-content: flex-start; } .dai-suggestion { display: inline-flex; align-items: center; gap: 8px; max-width: 100%; /* Trimmed from 8px 18px. At the 320px floor the old horizontal padding put 36px of empty pill around every label, which is what stopped two chips ever fitting side by side. */ padding: 7px 13px; text-align: left; /* A chip is one line. Without this the flex row can hand a chip a width narrower than its label and the text wraps inside the pill, which is what made the strip look ragged. The ellipsis itself is on .dai-suggestion-text — see there. */ white-space: nowrap; overflow: hidden; font: inherit; font-size: 13px; line-height: 1.4; font-weight: 500; color: #334155; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 9999px; box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03); cursor: pointer; transition: all 140ms ease; } .dai-suggestion:hover { background: #f8fafc; border-color: #cbd5e1; color: #0f172a; transform: translateY(-1px); box-shadow: 0 2px 5px rgba(15, 23, 42, 0.06); } .dai-suggestion:active { transform: translateY(0); } .dai-suggestion:focus-visible { outline: 2px solid #0f172a; outline-offset: 2px; } .dai-root .dai-suggestion-icon { flex: 0 0 auto; color: #64748b; transition: color 140ms ease; } .dai-suggestion:hover .dai-suggestion-icon { color: #0f172a; } .dai-root .dai-suggestion-text { /* The ellipsis lives here, not on the button: text-overflow needs the element that actually holds the text, and the button is a flex container whose children are the icon and this span. min-width:0 is what lets the span shrink below its content width so the ellipsis can engage at all — a flex item defaults to min-width:auto and would push the pill wider instead. The button already carries title={item.text}, so the full question is still available when a label is cut. */ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* Plain text link-button ("View more", "Sources") */ .dai-link { font: inherit; font-size: 12px; color: var(--dai-text-secondary); background: none; border: none; padding: 2px 0; cursor: pointer; align-self: flex-start; } .dai-link:hover { color: var(--dai-text); text-decoration: underline; } .dai-link:focus-visible { outline: 2px solid var(--dai-accent); outline-offset: 2px; border-radius: 5px; } /* -------------------------------------------------------------------------- Conversation -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- Scroll region -------------------------------------------------------------------------- */ .dai-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; /* Never scroll sideways. A single wide child (the create form's name row) used to push the whole message area horizontally. */ overflow-x: hidden; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--dai-border-strong) transparent; } .dai-scroll::-webkit-scrollbar { width: 8px; } .dai-scroll::-webkit-scrollbar-thumb { background: var(--dai-border-strong); border-radius: 999px; border: 2px solid transparent; background-clip: content-box; } .dai-scroll::-webkit-scrollbar-track { background: transparent; } .dai-scroll-wrap { position: relative; flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; } .dai-root .dai-thread { padding: 16px 14px 8px; } /* Only the newest turn animates in. Applying this to every .dai-msg meant opening a panel with a restored thread started 50 simultaneous opacity + transform animations, which is a visible hitch before the first scroll. The base state IS the animation's end state, so a turn that stops being last simply keeps its finished appearance. */ .dai-root .dai-thread > .dai-msg:last-child { animation: dai-enter 220ms var(--dai-ease) both; } @keyframes dai-enter { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } } .dai-root .dai-msg-user { max-width: 82%; margin-left: auto; padding: 8px 12px; border-radius: 5px; border-bottom-right-radius: 5px; font-size: 13.5px; line-height: 1.45; color: var(--dai-accent-contrast); background: var(--dai-accent); white-space: pre-wrap; overflow-wrap: anywhere; } /* Assistant — no bubble. Text sits on the panel surface. */ .dai-root .dai-msg-ai { font-size: 13.5px; line-height: 1.5; color: var(--dai-text); overflow-wrap: anywhere; } .dai-root .dai-msg-ai-detail { font-size: 12.5px; line-height: 1.55; color: var(--dai-text-secondary); white-space: pre-line; } .dai-root .dai-msg-name { font-size: 11.5px; font-weight: 600; color: var(--dai-text-secondary); } .dai-root .dai-msg-footer { font-size: 11px; color: var(--dai-text-muted); } /* Copy button — only revealed on hover of the message row. */ .dai-root .dai-msg-actions { opacity: 0; transition: opacity 140ms ease; } .dai-root .dai-msg-row:hover .dai-msg-actions, .dai-root .dai-msg-row:focus-within .dai-msg-actions { opacity: 1; } /* -------------------------------------------------------------------------- Structured metrics -------------------------------------------------------------------------- */ .dai-root .dai-stat-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; width: 100%; } .dai-root .dai-stat { padding: 9px 10px; border: 1px solid var(--dai-border); border-radius: 5px; background: var(--dai-surface-alt); } .dai-root .dai-stat-value { font-size: 19px; font-weight: 650; line-height: 1.1; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; } .dai-root .dai-stat-label { font-size: 11px; color: var(--dai-text-muted); } /* Headline metric — the "primary number is large" rule. */ .dai-root .dai-metric-value { font-size: 28px; font-weight: 680; line-height: 1.05; letter-spacing: -0.025em; color: var(--dai-text); font-variant-numeric: tabular-nums; } .dai-root .dai-metric-label { font-size: 12px; color: var(--dai-text-secondary); } /* -------------------------------------------------------------------------- Typing / loading -------------------------------------------------------------------------- */ .dai-root .dai-typing { display: inline-flex; align-items: center; gap: 4px; height: 18px; } .dai-root .dai-typing span { width: 5px; height: 5px; border-radius: 999px; background: var(--dai-text-muted); animation: dai-bounce 1.3s ease-in-out infinite; } .dai-root .dai-typing span:nth-child(2) { animation-delay: 0.16s; } .dai-root .dai-typing span:nth-child(3) { animation-delay: 0.32s; } @keyframes dai-bounce { 0%, 70%, 100% { opacity: 0.3; transform: translateY(0); } 35% { opacity: 0.9; transform: translateY(-3px); } } .dai-root .dai-shimmer { height: 9px; border-radius: 999px; background: linear-gradient(90deg, var(--dai-surface-hover) 25%, #e9eef5 37%, var(--dai-surface-hover) 63%); background-size: 400% 100%; animation: dai-shimmer 1.5s ease infinite; } @keyframes dai-shimmer { from { background-position: 100% 50%; } to { background-position: 0 50%; } } /* -------------------------------------------------------------------------- States (error / empty) -------------------------------------------------------------------------- */ .dai-root .dai-state { padding: 12px; border: 1px solid var(--dai-border); border-radius: 5px; background: var(--dai-surface-alt); } .dai-root .dai-state-title { font-size: 13px; font-weight: 600; color: var(--dai-text); } .dai-root .dai-state-body { font-size: 12.5px; line-height: 1.5; color: var(--dai-text-secondary); } .dai-root .dai-state-icon { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 5px; flex: 0 0 auto; } /* -------------------------------------------------------------------------- Jump-to-latest -------------------------------------------------------------------------- */ .dai-jump { position: absolute; left: 50%; bottom: 8px; transform: translateX(-50%); display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px; font: inherit; font-size: 12px; color: var(--dai-text); background: var(--dai-surface); border: 1px solid var(--dai-border-strong); border-radius: 999px; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.1); cursor: pointer; z-index: 2; } .dai-jump:hover { background: var(--dai-surface-alt); } .dai-jump:focus-visible { outline: 2px solid var(--dai-accent); outline-offset: 2px; } /* -------------------------------------------------------------------------- Composer -------------------------------------------------------------------------- */ .dai-root .dai-composer-wrap { flex: 0 0 auto; padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid #f1f5f9; background: #ffffff; } .dai-root .dai-composer { border: 1px solid #e2e8f0; border-radius: 9999px; background: #f8fafc; box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03); padding: 6px 6px 6px 16px; display: flex; align-items: center; gap: 8px; width: 100%; transition: border-color 140ms ease, box-shadow 140ms ease, background-color 140ms ease; } .dai-root .dai-composer[data-focused='true'] { background: #ffffff; border-color: #cbd5e1; box-shadow: 0 0 0 3px rgba(15, 23, 42, 0.05); } /* -------------------------------------------------------------------------- Conversation history. A list of rows, not cards: these are destinations, and dense rows scan faster than boxes when you are looking for one you remember. -------------------------------------------------------------------------- */ .dai-root .dai-history { padding: 4px 0; } .dai-root .dai-history-row { width: 100%; border-radius: 10px; padding-right: 4px; transition: background-color 140ms ease; } .dai-root .dai-history-row:hover { background: var(--dai-surface-alt); } .dai-root .dai-history-open { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; align-items: flex-start; text-align: left; padding: 9px 10px; border: none; background: transparent; font: inherit; cursor: pointer; border-radius: 10px; } .dai-root .dai-history-open:focus-visible { outline: 2px solid var(--dai-accent); outline-offset: -2px; } .dai-root .dai-history-title { font-size: 13px; font-weight: 500; color: var(--dai-text); display: block; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .dai-root .dai-history-meta { font-size: 11px; color: var(--dai-text-muted); } .dai-root .dai-history-empty-title { font-size: 13.5px; font-weight: 600; color: var(--dai-text); } .dai-root .dai-history-empty { font-size: 12.5px; line-height: 1.5; color: var(--dai-text-muted); } /* Microphone. Astryx's smallest dictation button is 28px, two short of the send button it sits beside — a visible step between two controls on the same row. Pinned to match, with the glyph scaled through font-size because the icon inside is sized in em units (the same reason the side nav icons needed font-size rather than width). */ .dai-root .dai-mic { width: 30px; height: 30px; min-width: 30px; border-radius: 50%; font-size: 16px; } .dai-root .dai-mic svg { width: 1em; height: 1em; } /* -------------------------------------------------------------------------- Send button. This had NO styles at all — the class was referenced once, in a reduced-motion reset, and nowhere else. It rendered as a bare 16px arrow glyph on a transparent background with square corners: not a button, just an icon sitting next to the microphone. Verified in the browser before fixing. -------------------------------------------------------------------------- */ .dai-root .dai-send { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; width: 32px; height: 32px; padding: 0; border: none; border-radius: 50%; background: #2563eb; color: #ffffff; cursor: pointer; transition: background-color 140ms ease, opacity 140ms ease, transform 140ms ease; } .dai-root .dai-send:hover:not(:disabled) { background: #1d4ed8; transform: scale(1.05); } .dai-root .dai-send:active:not(:disabled) { transform: scale(0.96); } .dai-root .dai-send:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; } /* Disabled is the resting state until something is typed, so it has to read as "not yet", not as "broken" — a filled grey circle, same shape and weight. */ .dai-root .dai-send:disabled { background: #f1f5f9; color: #94a3b8; cursor: default; } .dai-root .dai-composer textarea { display: block; flex: 1 1 auto; width: 100%; min-width: 0; border: none; outline: none; resize: none; padding: 4px 0; margin: 0; font: inherit; font-size: 13.5px; line-height: 1.45; color: var(--dai-text); background: transparent; max-height: 108px; /* ~5 lines */ overflow-y: auto; } .dai-root .dai-composer textarea::placeholder { color: var(--dai-text-muted); } .dai-root .dai-hint { font-size: 10.5px; line-height: 1.3; color: var(--dai-text-muted); padding-inline: 2px; } /* -------------------------------------------------------------------------- The trigger's styles lived here and are gone with it. `.dai-trigger` / `.dai-trigger-mark` styled DoormileAI/index.jsx, a trigger button component that nothing rendered — AdminLayout has always declared its own button in JSX with Tailwind classes. Component and CSS removed together so neither is left looking like the thing that controls the header button. To change that button, edit AdminLayout. -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- Responsive -------------------------------------------------------------------------- */ /* Tablet and small laptop, 768-900px: the panel is a full-width overlay with a scrim (see the max-width:900px block above), so the only thing left to do is stop the suggestion strip from taking a third of a short screen — there is much less height to spend here than on a desktop. */ @media (max-width: 900px) { .dai-root .dai-suggestions-bar { max-height: 26vh; } } /* The block that used to sit here set `--dai-panel-width: 396px` at 1279px. That variable is declared and never read — the panel sizes from --dai-dock-width — so the rule had no effect on anything. Removed rather than left looking load-bearing. */ @media (max-width: 767px) { .dai-root { --dai-inset: 0px; } /* Phone: chips get a touch target and room to breathe, and the strip is capped harder still because the on-screen keyboard takes the bottom half of the viewport the moment the composer is focused. */ .dai-root .dai-suggestions-bar { max-height: 22vh; padding: 8px 12px 6px; } .dai-suggestion { padding: 9px 14px; font-size: 13px; } .dai-panel { width: 100vw; max-width: 100vw; border-radius: 0; border: none; } } /* -------------------------------------------------------------------------- Reduced motion — no slide, no pulse, no shimmer. Opacity only. -------------------------------------------------------------------------- */ @media (prefers-reduced-motion: reduce) { .dai-panel, .dai-scrim { transition: opacity 1ms linear; } .dai-panel { transform: none; } .dai-msg, .dai-live-dot, .dai-typing span, .dai-shimmer, .dai-suggestion, .dai-send { animation: none !important; transition: none !important; } } /* -------------------------------------------------------------------------- Write-action confirm card & buttons -------------------------------------------------------------------------- */ .dai-root .dai-action { padding: 12px 14px; border: 1px solid #e2e8f0; border-radius: 12px; background: #ffffff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02); display: flex; flex-direction: column; gap: 10px; } .dai-root .dai-action-icon { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; width: 28px; height: 28px; border-radius: 8px; color: #4f46e5; background: rgba(99, 102, 241, 0.1); } .dai-root .dai-action-summary { font-size: 13px; line-height: 1.4; font-weight: 600; color: var(--dai-text); } .dai-root .dai-action-note { font-size: 12px; color: #64748b; line-height: 1.4; } .dai-root .dai-ok { color: #047857; font-weight: 500; } .dai-root .dai-err { color: #b91c1c; font-weight: 500; } .dai-root .dai-action button { cursor: pointer; } /* -------------------------------------------------------------------------- Overflow containment -------------------------------------------------------------------------- Flex items default to `min-width: auto`, which means they refuse to shrink below their content's intrinsic width. Two side-by-side TextInputs in the create-customer form therefore pushed the message column wider than the panel and produced a horizontal scrollbar. Every link in the chain from the scroll region down to the field has to opt out of that. -------------------------------------------------------------------------- */ .dai-root .dai-thread, .dai-root .dai-msg, .dai-root .dai-msg-row, .dai-root .dai-action, .dai-root .dai-form { min-width: 0; max-width: 100%; } /* Two fields per row that genuinely share the width. `flex-wrap` is the belt-and-braces part: if a field ever can't compress far enough (a long label, a narrower panel), the row drops to two lines instead of pushing the panel sideways again. */ .dai-root .dai-form-row { display: flex; flex-wrap: wrap; gap: 8px; width: 100%; min-width: 0; } .dai-root .dai-form-row > * { flex: 1 1 140px; min-width: 0; } /* Astryx's TextInput sizes itself to content unless told otherwise. */ .dai-root .dai-form input, .dai-root .dai-form-row input { width: 100%; min-width: 0; box-sizing: border-box; } .dai-root .dai-field-err { font-size: 11.5px; line-height: 1.4; color: #b91c1c; } /* -------------------------------------------------------------------------- Source footer + full lists -------------------------------------------------------------------------- */ /* Timestamp left, Sources toggle hard right, both on ONE line. It was a wrapping baseline row, and the toggle — a shrinkable flex item with inline content — gave up its chevron to a second line whenever the panel got narrow, which read as a stray arrow floating under the text. */ .dai-root .dai-msg-foot { display: flex; align-items: center; flex-wrap: nowrap; gap: 10px; width: 100%; min-width: 0; } /* The timestamp is the only part allowed to give up space, and it ellipsises rather than wrapping. */ .dai-root .dai-msg-foot .dai-msg-footer { flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } /* The toggle is one atom: label and chevron cannot be separated. */ .dai-root .dai-msg-foot .dai-sources { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px; align-self: center; white-space: nowrap; padding: 3px 7px; margin-right: -7px; border-radius: 5px; color: var(--dai-text-muted); transition: background 140ms ease, color 140ms ease; } .dai-root .dai-msg-foot .dai-sources:hover { background: var(--dai-surface-hover); color: var(--dai-text); text-decoration: none; } .dai-root .dai-msg-foot .dai-sources svg { flex: 0 0 auto; transition: transform 160ms var(--dai-ease); } .dai-root .dai-msg-foot .dai-sources[aria-expanded='true'] svg { transform: rotate(180deg); } /* A full, readable list instead of "…and 4 more". Scrolls past ~12 rows so a long list can never push the panel's height around. */ .dai-root .dai-list { width: 100%; min-width: 0; border: 1px solid var(--dai-border); border-radius: 5px; background: var(--dai-surface-alt); overflow: hidden; } .dai-root .dai-list-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; padding: 7px 10px; border-bottom: 1px solid var(--dai-border); font-size: 11px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--dai-text-muted); } .dai-root .dai-list-body { max-height: 260px; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; } .dai-root .dai-list-item { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; padding: 6px 10px; font-size: 12.5px; line-height: 1.4; color: var(--dai-text); border-top: 1px solid var(--dai-divider, rgba(15, 23, 42, 0.05)); } .dai-root .dai-list-item:first-child { border-top: none; } .dai-root .dai-list-index { flex: 0 0 auto; min-width: 18px; font-size: 11px; font-variant-numeric: tabular-nums; color: var(--dai-text-muted); } /* The label must not be crushed to zero width by a long value — that is what made "address" render one letter per line. It sizes to its content and the value takes the remaining space, wrapping onto its own full-width line when there isn't enough room for both. */ .dai-root .dai-list-label { flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere; } .dai-root .dai-list-meta { flex: 1 1 auto; min-width: 0; text-align: right; font-size: 11.5px; color: var(--dai-text-secondary); overflow-wrap: anywhere; } /* A value that wrapped to its own line reads better left-aligned under its label than pinned to the right edge. */ .dai-root .dai-list-item > .dai-list-meta:only-child, .dai-root .dai-list-meta[data-wrapped='true'] { text-align: left; } /* -------------------------------------------------------------------------- Documentation answers -------------------------------------------------------------------------- Retrieved passages are shown VERBATIM with their source file. They are prose rather than a figure, so they get a quieter, wider treatment than a metric — and the source line matters as much as the text, because the operator needs to know this came from a document, not from live data. -------------------------------------------------------------------------- */ .dai-root .dai-doc { padding: 10px 11px; border: 1px solid var(--dai-border); border-left: 2px solid var(--dai-ai-from); border-radius: 5px; background: var(--dai-surface-alt); font-size: 12.5px; line-height: 1.55; color: var(--dai-text); white-space: pre-line; overflow-wrap: anywhere; max-height: 320px; overflow-y: auto; } .dai-root .dai-doc-source { font-size: 11px; color: var(--dai-text-muted); font-variant-numeric: tabular-nums; }