Files
Doormilexpress_console/src/pages/nearle/assistant/DoormileAI.css

1124 lines
33 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ==========================================================================
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 <body> 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;
}