/* ========================================================================== 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 { /* 30vw, NOT 30%. The two consumers resolve a percentage against different boxes: the panel is position:fixed, so `width: 30%` is 30% of the VIEWPORT, while `padding-right: 30%` on the content container is 30% of ITS containing block — which is narrower by the side nav. At 1440px that is 432px of panel against 410px of reserved strip, and the page slides 22px underneath the panel. 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. */ /* 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. */ --dai-dock-width: max(300px, 25vw); --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: 428px; --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; /* Set from JS by AIPanel — Astryx does not publish a header-height token, despite --appshell-header-height looking like one. */ top: var(--dai-dock-top, 57px); right: 0; bottom: 0; z-index: 1200; width: var(--dai-dock-width); display: flex; flex-direction: column; min-height: 0; overflow: hidden; background: var(--dai-surface); border-left: 1px solid var(--dai-border); border-radius: 0; box-shadow: -6px 0 20px rgba(15, 23, 42, 0.05); transform: translateX(100%); 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. */ .astryx-layout-content { transition: padding-right var(--dai-duration) var(--dai-ease); } body.dai-docked .astryx-layout-content { padding-right: var(--dai-dock-width); } /* ---- 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; width: 100%; z-index: 1301; border-left: none; box-shadow: var(--dai-shadow); } .dai-scrim { display: block; } .dai-scrim[data-open='true'] { opacity: 1; } 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 12px 12px 14px; border-bottom: 1px solid var(--dai-border); } .dai-root .dai-title { font-size: 15px; font-weight: 650; line-height: 1.2; letter-spacing: -0.01em; color: var(--dai-text); } .dai-root .dai-subtitle { font-size: 12px; line-height: 1.3; color: var(--dai-text-muted); } /* 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: 999px; background: var(--dai-surface); 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 { flex: 0 0 auto; padding: 7px 14px; font-size: 11.5px; color: var(--dai-text-muted); background: var(--dai-surface-alt); border-bottom: 1px solid var(--dai-border); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* -------------------------------------------------------------------------- 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 { width: 100%; } .dai-suggestion { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 5px 10px; text-align: left; font: inherit; font-size: 12px; line-height: 1.35; font-weight: 500; color: var(--dai-text); background: var(--dai-surface); border: 1px solid var(--dai-border); /* 14px, pinned — NOT 999px. On a one-line chip a fully round radius already resolves to about 14px, so these look identical. The difference shows on a chip whose question wraps to two lines: 999px would resolve to half of ~46px and the chip stops reading as a chip and starts reading as a card, so one long suggestion would look like a different component from the nineteen beside it. */ border-radius: 14px; cursor: pointer; transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms ease; } .dai-suggestion:hover { background: var(--dai-surface-alt); border-color: var(--dai-border-strong); transform: translateY(-1px); } .dai-suggestion:active { transform: translateY(0); } .dai-suggestion:focus-visible { outline: 2px solid var(--dai-accent); outline-offset: 2px; } .dai-root .dai-suggestion-icon { flex: 0 0 auto; color: var(--dai-text-muted); transition: color 140ms ease; } .dai-suggestion:hover .dai-suggestion-icon { color: var(--dai-text-secondary); } .dai-root .dai-suggestion-text { min-width: 0; } /* 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: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid var(--dai-border); background: var(--dai-surface); } .dai-root .dai-composer { border: 1px solid var(--dai-border-strong); /* 14px, the same corner the suggestion chips use. At 5px the input was the one sharp-cornered thing in a panel of rounded surfaces, and it sat directly beneath the chips where the mismatch was most visible. */ border-radius: 14px; background: var(--dai-surface); box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); padding: 10px 10px 8px 14px; transition: border-color 140ms ease, box-shadow 140ms ease; } .dai-root .dai-composer[data-focused='true'] { border-color: var(--dai-accent); box-shadow: 0 0 0 3px var(--dai-accent-ring); } /* 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: 30px; height: 30px; padding: 0; border: none; border-radius: 50%; background: var(--dai-accent); color: var(--dai-accent-contrast); cursor: pointer; transition: background-color 140ms ease, opacity 140ms ease, transform 140ms ease; } .dai-root .dai-send:hover:not(:disabled) { opacity: 0.86; } .dai-root .dai-send:active:not(:disabled) { transform: scale(0.94); } .dai-root .dai-send:focus-visible { outline: 2px solid var(--dai-accent); 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: var(--dai-surface-hover); color: var(--dai-text-muted); cursor: default; } .dai-root .dai-composer textarea { display: block; width: 100%; border: none; outline: none; resize: none; padding: 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; } /* -------------------------------------------------------------------------- Trigger (lives in the app TopNav) -------------------------------------------------------------------------- */ .dai-trigger { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; padding: 0; border-radius: 5px; border: 1px solid transparent; background: transparent; cursor: pointer; overflow: hidden; transition: background-color 140ms ease, border-color 140ms ease, transform 140ms ease; } /* 30px box × 1.5 cancels the mark's transparent padding, so the visible D is 30px inside the 40px button. */ .dai-trigger-mark { width: 30px; height: 30px; max-width: none; object-fit: contain; display: block; transform: scale(1.5); } .dai-trigger:hover { background: rgba(0, 0, 0, 0.05); border-color: rgba(0, 0, 0, 0.1); } .dai-trigger:active { transform: scale(0.94); } .dai-trigger:focus-visible { outline: 2px solid var(--dai-accent); outline-offset: 2px; } .dai-trigger[data-active='true'] { background: rgba(0, 0, 0, 0.07); border-color: rgba(0, 0, 0, 0.14); } /* -------------------------------------------------------------------------- Responsive -------------------------------------------------------------------------- */ @media (max-width: 1279px) { .dai-root { --dai-panel-width: 396px; } } @media (max-width: 767px) { .dai-root { --dai-inset: 0px; } .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, .dai-trigger { animation: none !important; transition: none !important; } } /* -------------------------------------------------------------------------- Write-action confirm card -------------------------------------------------------------------------- */ .dai-root .dai-action { padding: 10px 11px; border: 1px solid var(--dai-border-strong); border-radius: 5px; background: var(--dai-surface-alt); } .dai-root .dai-action-icon { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; width: 26px; height: 26px; border-radius: 5px; color: #4f46e5; background: rgba(99, 102, 241, 0.1); } .dai-root .dai-action-summary { font-size: 13px; line-height: 1.35; font-weight: 550; color: var(--dai-text); } .dai-root .dai-action-note { font-size: 12px; color: var(--dai-text-secondary); } .dai-root .dai-ok { color: #047857; } .dai-root .dai-err { color: #b91c1c; } /* -------------------------------------------------------------------------- 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; }