/* See the Tailwind configuration guide for advanced usage
https://tailwindcss.com/docs/configuration */
/* There is exactly one component system here: Basecoat structure plus OpenAgents
* style pack. DaisyUI was removed (see AGENTS.md) — it emitted flat component
* rules from its own cascade layer that outranked every `.btn[data-variant=…]`
* in openagents.css, so all eight UI button variants rendered identically. Do
* not reintroduce it, and do not add a second component library beside it.
*
* With DaisyUI gone the layer order Tailwind itself declares
* (`theme, base, components, utilities`) is correct as-is, so this file no
* longer restates it. */
@import "tailwindcss" source(none);
@import "phoenix-colocated/openagents/colocated.css";
/* Basecoat component structure, vendored at tag 1.0.2 (6953a4a). Import
* components individually and only when a surface uses one: component CSS
* lives in @layer components and is emitted whether or not the class appears
* in markup. Never import basecoat.css, basecoat-base.css, or
* basecoat-components.css. See assets/vendor/basecoat/README.md.
*
* These carry the structure (display, padding, min-height, background) that
* openagents.css deliberately does not set — its .btn only adds gap, border,
* radius, colour, font, and transition on top. With DaisyUI gone they are the
* only source of that geometry, so nothing here is optional. This list is the
* exact structure consumed by `OpenAgentsWeb.UI`. */
@import "../vendor/basecoat/base/base.css";
@import "../vendor/basecoat/components/breadcrumb.css";
@import "../vendor/basecoat/components/button.css";
@import "../vendor/basecoat/components/button-group.css";
@import "../vendor/basecoat/components/input.css";
@import "../vendor/basecoat/components/textarea.css";
@import "../vendor/basecoat/components/label.css";
@import "../vendor/basecoat/components/field.css";
@import "../vendor/basecoat/components/form.css";
@import "../vendor/basecoat/components/alert.css";
@import "../vendor/basecoat/components/badge.css";
@import "../vendor/basecoat/components/card.css";
@import "../vendor/basecoat/components/avatar.css";
@import "../vendor/basecoat/components/item.css";
@import "../vendor/basecoat/components/empty.css";
@import "../vendor/basecoat/components/kbd.css";
@import "../vendor/basecoat/components/table.css";
/* OpenAgents identity. Must stay last so its declarations win. */
@import "./openagents.css";
@source "../css";
@source "../js";
@source "../../lib/openagents_web";
/* Required for Tailwind to automatically pick up changes in colocated CSS files in dev */
@source "../../_build/dev/phoenix-colocated/openagents/*/";
/* Deliberate second-tier icon fallback. Product call sites prefer the vendored
* Apps SDK set; docs/ICONS.md inventories every exceptional Heroicons use. */
@plugin "../vendor/heroicons";
/* ── Token ladder ───────────────────────────────────────────────────────────
* Integrated into OpenAgents, where these are the two tiers of DESIGN.md "Color
* Tokens": PRIMITIVES are the hand-picked Aiur oklch values; DERIVATIONS are
* color-mix() steps off one ink, so the hover/focus/border grammar is
* systematic rather than accumulated.
*
* openagents.css consumes these but does not define them — it aliases them onto
* the Basecoat names (`--background`, `--card`, `--primary`, `--border`, …)
* in its own `:root`. Before this block existed those aliases resolved to
* nothing and the DaisyUI theme was carrying the palette instead, which is
* exactly the collision that made every UI variant render identically.
*
* The three surfaces are a fixed relationship, not three loose colors: page
* and sidebar sit on the darkest (--ink-void), the working canvas one step up
* (--ink-surface), elevated components another step up (--ink-raised). New
* surfaces take a rung; they do not invent a fourth.
*
* Two themes. `:root` carries dark; `:root[data-theme="light"]` overrides only
* the primitives and the few literal exceptions. Everything else in the ladder
* is `color-mix(... var(--text-ink) N% ...)`, so the icon tiers, washes, hover
* surface and inverse control invert on their own when the ink flips — that is
* the whole point of deriving them rather than listing them twice.
*
* The ramp is neutral to faintly cool, never blue. The previous palette was
* tinted (chroma ~.03-.07 around hue 263), which read as blue on every
* surface -- borders, muted text, and each of the three ink rungs.
*/
:root {
color-scheme: dark;
/* PRIMITIVES — the dark ramp.
Three surface rungs, each a small step up in lightness: page and sidebar on
void, the working canvas on surface, elevated components on raised. Raised
is LIGHTER than void; light mode keeps that direction rather than flipping
it, so a card reads as nearer the reader in both themes. */
--ink-void: #08090a; /* darkest: page, sidebar */
--ink-surface: #0f1011; /* +1: canvas, chrome */
--ink-raised: #141516; /* +2: elevated components */
--text-ink: #f7f8f8; /* the one text/icon/wash ink (their --color-fg-primary) */
/* The single accent, used only for the primary action and links; every other
surface is neutral.
NAME COLLISION, deliberate: Basecoat's base.css also defines --accent, but
as shadcn does — a subtle hover background paired with --accent-foreground,
not a brand color. Ours is defined later at equal specificity so it wins.
Only combobox.css, dropdown-menu.css and select.css consume Basecoat's
meaning and none of the three is imported; importing one would give it an
indigo hover surface. Rename ours before importing them. */
--accent: #5e6ad2;
--accent-bright: #828fff;
--info: #4ea7fc;
--success: #27a644;
--done: #ab7df8; /* GitHub's closed-as-completed purple */
--warning: #d4b144;
--danger: #eb5757;
/* DERIVATIONS — text tiers. The percentages beside each literal are the
equivalent alpha step, kept so the ladder stays legible if a later theme
wants to derive these rather than list them. */
--text-primary: color-mix(in oklab, var(--text-ink) 100%, transparent); /* 100% */
--text-body: #d0d6e0; /* their fg-secondary */
--text-muted: #8a8f98; /* their fg-tertiary */
--text-dim: #62666d; /* their fg-quaternary */
/* Icon tiers. Glyphs read lighter than text at the same alpha, so they get
their own ladder rather than borrowing the text one. */
--icon-primary: color-mix(in oklab, var(--text-ink) 100%, transparent); /* 100% */
--icon-secondary: color-mix(in oklab, var(--text-ink) 66%, transparent); /* 66% */
--icon-tertiary: color-mix(in oklab, var(--text-ink) 52%, transparent); /* 52% */
--icon-faint: color-mix(in oklab, var(--text-ink) 28%, transparent); /* 28% */
/* Washes — translucent ink laid over whatever surface is beneath, so hover
and selection read as the same gesture on every rung of the surface
ladder. Hover < active < selected < strong. */
--wash-hover: color-mix(in oklab, var(--text-ink) 6%, transparent); /* 6% */
--wash-active: color-mix(in oklab, var(--text-ink) 8%, transparent); /* 8% */
--wash-selected: color-mix(in oklab, var(--text-ink) 10%, transparent); /* 10% */
--wash-strong: color-mix(in oklab, var(--text-ink) 14%, transparent); /* 14% */
/* The scrim behind a modal. Deliberately NOT built from `--ink-void`, which
is the page and flips to near-white in light mode: a scrim's job is to
darken whatever is behind it, and a white one over a white page hides
nothing. Fixed dark in both themes, which is why it is a token of its own
rather than a reference to another. */
--scrim: color-mix(in oklab, #08090a 55%, transparent);
/* The hover surface is the wash resolved opaquely over the raised rung. */
--ink-hover: color-mix(in oklab, var(--text-ink) 4%, var(--ink-raised)); /* 4% over raised */
/* The one inverted control: the strongest ink as ground, the darkest rung as
glyph. Flips by itself in light mode — dark fill, light glyph. */
--control-inverse-bg: var(--text-primary);
--control-inverse-fg: var(--ink-void);
/* Borders. Two rungs: a resting hairline and a stronger edge for controls. */
--line: #23252a; /* their --color-border-primary */
--line-soft: color-mix(in oklab, var(--text-ink) 8%, transparent); /* 8% */
--line-faint: color-mix(in oklab, var(--text-ink) 4%, transparent); /* 4% */
--line-strong: #34343a; /* their --color-border-secondary */
}
/* LIGHT.
*
* Derived from the dark ramp rather than authored separately: the same neutral
* hue discipline, the same surface-ladder direction (raised is lighter than
* void), and the ink flipped to the darkest rung.
*
* Only the primitives and the four literal exceptions are restated. The icon
* tiers, washes, hover surface and inverse control are all derived from
* --text-ink above and invert without being repeated here.
*/
:root[data-theme="light"] {
color-scheme: light;
--ink-void: #f7f8f8; /* page, sidebar — the most recessive surface */
--ink-surface: #fbfbfc; /* +1: canvas */
--ink-raised: #ffffff; /* +2: elevated components read as paper */
--text-ink: #08090a; /* the ink is now their darkest value */
/* Indigo darkens for contrast against white; the bright rung becomes the
hover/visited state rather than the resting one. */
--accent: #5e6ad2;
--accent-bright: #4a55b8;
--done: #8250df; /* GitHub's light-theme closed-as-completed purple */
--text-body: #3c4149;
--text-muted: #62666d;
--text-dim: #8a8f98;
--line: #e6e7e9;
--line-strong: #d0d2d6;
}
/* No-JS / no-stored-preference fallback. The head script sets data-theme
* before first paint, so this only governs the window before it runs and the
* case where scripting is off. It is scoped :not([data-theme]) so an explicit
* choice always wins over the OS preference. */
@media (prefers-color-scheme: light) {
:root:not([data-theme]) {
color-scheme: light;
--ink-void: #f7f8f8;
--ink-surface: #fbfbfc;
--ink-raised: #ffffff;
--text-ink: #08090a;
--accent: #5e6ad2;
--accent-bright: #4a55b8;
--text-body: #3c4149;
--text-muted: #62666d;
--text-dim: #8a8f98;
--line: #e6e7e9;
--line-strong: #d0d2d6;
}
}
/* The semantic four are OpenAgents, not DaisyUI's, and have no Basecoat
* equivalent — `--color-destructive` covers danger, but there is no success,
* warning, or info in the Basecoat base. */
@theme {
--color-success: var(--success);
--color-warning: var(--warning);
--color-info: var(--info);
--color-danger: var(--danger);
}
/* Add variants based on LiveView classes */
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
/* Make LiveView wrapper divs transparent for layout */
[data-phx-session], [data-phx-teleported-src] { display: contents }
/* This file is for your main application CSS */
/* ── Sidebar (OpenAgents layout with OpenAgents color tokens) ───────────── */
/* One rule rather than two kept identical by hand. The previous pair carried a
comment asserting they matched while they had already drifted on gap and
padding, so the docs rail sat 12px lower with 8px more between its parts
than the application rail. A shared selector cannot drift. */
.sidebar,
.docs-sidebar {
display: flex;
min-height: 0;
flex: none;
flex-direction: column;
width: 240px;
height: 100%;
overflow: hidden;
border-right: 1px solid var(--line);
background: var(--ink-void);
}
/* The application sidebar is a drawer below the desktop breakpoint and a
collapsible rail above it. The server renders it closed so mobile never
paints an open drawer before JavaScript starts. On desktop, the no-JavaScript
fallback remains visible. */
#app-shell > .sidebar {
position: fixed;
inset-block: 0;
inset-inline-start: 0;
z-index: 50;
width: min(280px, calc(100vw - 48px));
padding-inline-start: env(safe-area-inset-left);
visibility: hidden;
transform: translateX(-100%);
transition:
transform 200ms cubic-bezier(0.22, 1, 0.36, 1),
visibility 0s linear 200ms;
}
#app-shell[data-sidebar-open="true"] > .sidebar {
visibility: visible;
transform: translateX(0);
transition-delay: 0s;
}
.sidebar-scrim {
position: fixed;
inset: 0;
z-index: 40;
display: none;
border: 0;
background: rgb(0 0 0 / 58%);
}
#app-shell[data-sidebar-open="true"] > .sidebar-scrim {
display: block;
}
body.sidebar-open {
overflow: hidden;
}
.sidebar-toggle {
display: inline-flex;
flex: none;
width: 36px;
height: 36px;
min-width: 36px;
min-height: 36px;
align-items: center;
justify-content: center;
border-radius: var(--radius-lg);
color: var(--text-muted);
cursor: w-resize;
transition:
background-color 150ms var(--ease),
color 150ms var(--ease);
}
.sidebar-toggle .icon {
width: 18px;
height: 18px;
}
@media (hover: hover) {
.sidebar-toggle:hover {
background: color-mix(in srgb, var(--text-primary) 10%, transparent);
color: var(--text-primary);
}
}
.sidebar-app-header {
display: flex;
flex: none;
align-items: center;
min-height: 52px;
padding-inline-end: 8px;
}
.sidebar-app-header .sidebar-brand {
flex: 1;
min-width: 0;
}
#app-shell[data-sidebar-initialized="true"][data-sidebar-open="true"] .sidebar-toggle--expand,
#app-shell[data-sidebar-initialized="true"][data-sidebar-open="false"] .sidebar-toggle--collapse {
display: none;
}
@media (min-width: 1024px) {
#app-shell:not([data-sidebar-initialized="true"]) .sidebar-toggle--expand {
display: none;
}
#app-shell > .sidebar {
position: relative;
inset: auto;
z-index: auto;
width: 240px;
padding-inline-start: 0;
visibility: visible;
transform: none;
transition:
width 200ms cubic-bezier(0.22, 1, 0.36, 1),
border-color 200ms ease;
}
#app-shell[data-sidebar-initialized="true"][data-sidebar-open="false"] > .sidebar {
width: 0;
border-color: transparent;
visibility: hidden;
}
#app-shell > .sidebar-scrim,
#app-shell[data-sidebar-open="true"] > .sidebar-scrim {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
#app-shell > .sidebar {
transition: none;
}
}
.sidebar-header {
display: flex;
flex: none;
align-items: center;
gap: 10px;
/* Matches .docs-header, so the brand and the breadcrumb sit on one line
across the two surfaces. */
min-height: 52px;
padding: 8px 20px;
}
.brand-name {
flex: none;
font-size: 1rem;
font-weight: 700;
color: var(--text-primary);
}
.sidebar-nav {
display: flex;
flex: none;
flex-direction: column;
gap: 1px;
/* No block padding. A sticky section heading sticks to the scrollport edge,
and any padding above that edge is a band the heading cannot cover, so
rows scrolled through it. Removing the gap is the fix; painting over it
with a pseudo-element put an opaque box inside a z-indexed sticky element,
which is a good way to cover the heading's own text. */
}
.sidebar-nav[aria-label="OpenAgents tools"] {
margin-top: auto;
}
.sidebar-row {
position: relative;
display: flex;
align-items: center;
gap: 12px;
/* 32px rows sitting 1px apart. The previous 36px row with 6px of block
padding and 4px between rows spent roughly half the sidebar's height on
space between things rather than on the things. */
min-height: 32px;
margin-inline: 12px;
padding-inline: 8px;
padding-block: 0;
border-radius: 6px;
/* Rest dim, brighten on hover. A sidebar is a list of places you are not, so
at full brightness every row competes with the page for attention. The
current row and the hovered row are the two that earn it. */
color: var(--text-muted);
font-size: 0.875rem;
font-weight: 400;
line-height: 1.25rem;
transition:
background-color 150ms ease,
color 150ms ease;
}
@media (hover: hover) {
.sidebar-row:hover {
background: var(--wash-hover);
/* All the way to the brightest ink. A one-step lift to --text-body was
there but unreadable: the wash arrives at the same moment, so a small
text change reads as "the background moved" and nothing else. Hover and
the current row now share a text colour and are told apart by the
background -- selected carries a stronger, persistent wash. */
color: var(--text-primary);
}
}
.sidebar-row:has(.sidebar-row__hit:active) {
background: var(--wash-active);
}
.sidebar-row[data-selected] {
/* A wash like hover, one step stronger. Using an opaque rung made the current
row a different surface floating on the sidebar instead of a state of it. */
background: var(--wash-selected);
/* The place you are is the one row that stays bright without being hovered. */
color: var(--text-primary);
}
.sidebar-row--static,
.sidebar-row--empty {
cursor: default;
}
.sidebar-row--static:hover,
.sidebar-row--empty:hover {
background: transparent;
/* Not a destination, so hovering it must not imply it is one. */
color: var(--text-muted);
}
.sidebar-row__hit {
position: absolute;
z-index: 1;
inset: 0;
padding: 0;
border: 0;
border-radius: inherit;
background: transparent;
cursor: pointer;
}
.sidebar-row__content {
position: relative;
display: flex;
min-width: 0;
align-items: center;
gap: 0;
pointer-events: none;
}
/* Smaller than the row's type and held further from it. A 20px glyph beside
14px text reads as the larger of the two, which inverts what the row is
about: the word is the destination and the glyph is a hint. */
.sidebar-row__icon {
display: inline-flex;
width: 16px;
height: 16px;
flex: none;
align-items: center;
justify-content: center;
/* Stated here rather than as a gap on an ancestor. The nav rows took their
spacing from `.sidebar-row__content`'s 6px gap and the footer links from
their own 12px, so the two columns sat at different distances from their
labels while looking like the same control. */
margin-right: 10px;
color: currentColor;
font-size: 16px;
}
/* Held at the far end of the row, past the label. Positioned rather than laid
out with a gap, because the label is what shrinks when the row runs out of
width and the count is the part that must stay legible. Pointer events go to
the row's hit area underneath, so the count is never a second click target
for the same destination. */
.sidebar-row__badge {
position: relative;
margin-left: auto;
padding-left: 8px;
font-variant-numeric: tabular-nums;
pointer-events: none;
}
.sidebar-row__label {
/* `overflow: hidden` is what makes the ellipsis work, and it also makes this
span a block whose height is exactly its line box. At the row's 20px line
height that box ends on the baseline's shoulder, so the tails of g, j, p,
q and y were sliced off. The line box gets 4px more to sit in; the row's
own height is set by `min-height`, so nothing moves. */
overflow: hidden;
line-height: 1.5rem;
white-space: nowrap;
text-overflow: ellipsis;
}
.sidebar-section {
position: relative;
/* A section heading needs air above it or it reads as one more row in the
list it is naming. The nav lost its block padding when the sticky heading
was fixed, and nothing replaced it, so "Sarah" sat hard against the row
above. The space belongs to the section rather than the nav, which is why
it survives a heading sticking to the scrollport edge. */
margin-block-start: 16px;
}
/* A section that opens the rail has nothing above it to be held off from. */
.sidebar-section:first-child {
margin-block-start: 0;
}
/* Only an open section needs holding off the next one. Paying that gap while
collapsed made a rail of collapsed sections carry a separator between rows
that were already just rows, which is most of why it read loose. */
.sidebar-section[open] {
padding-block-end: 6px;
}
/* Declared here, not in the style pack. `.sidebar-section-label` is unlayered
and sets `color`, so the layered `.sidebar-section__summary:hover` could
never win and the heading was the one sidebar row that did not respond to a
pointer. */
@media (hover: hover) {
.sidebar-section-label:hover {
color: var(--text-primary);
}
}
.sidebar-section-label {
position: sticky;
z-index: 1;
top: 0;
display: flex;
align-items: center;
/* A section heading is a row you can click, so it is sized like one: the
SAME height as `.sidebar-row`, not a taller near-match. At 36px against a
20px line it carried 8px of air per side while the row beside it carried
6px, and the mismatch is what made the column look airier vertically than
it is horizontally. Uppercasing it with tracking made it shout a word the
reader already scanned past. */
min-height: 32px;
/* Same reason as `.sidebar-row__label`: at 20px the line box ends on the
baseline's shoulder and the tails of g, y and p sit on the edge of it. */
line-height: 1.5rem;
margin-inline: 12px;
padding-inline: 8px;
/* Sticky, so it needs an opaque fill for rows to scroll under -- but it must
be the SIDEBAR's fill. Left on --ink-surface it drew a lighter bar across
a darker sidebar, which read as a panel rather than a heading. */
background: var(--ink-void);
color: var(--text-muted);
font-size: 0.875rem;
font-weight: 400;
}
/* Docs layout */
.docs-layout {
display: flex;
/* Dynamic viewport units, for the reason the application shell states: on a
mobile browser `100vh` measures the viewport without the chrome, so the
last row of an internally scrolling surface sits under the toolbar. */
height: 100dvh;
overflow: hidden;
}
.docs-sidebar-scrim {
position: fixed;
inset: 0;
z-index: 40;
display: none;
border: 0;
background: rgb(0 0 0 / 58%);
}
#docs-shell[data-sidebar-open="true"] > .docs-sidebar-scrim {
display: block;
}
body.docs-sidebar-open {
overflow: hidden;
}
#docs-shell[data-sidebar-initialized="true"][data-sidebar-open="true"] .sidebar-toggle--expand,
#docs-shell[data-sidebar-initialized="true"][data-sidebar-open="false"] .sidebar-toggle--collapse {
display: none;
}
#docs-shell > .docs-sidebar {
transition:
width 200ms cubic-bezier(0.22, 1, 0.36, 1),
border-color 200ms ease;
}
@media (min-width: 1024px) {
#docs-shell:not([data-sidebar-initialized="true"]) .sidebar-toggle--expand {
display: none;
}
#docs-shell[data-sidebar-initialized="true"][data-sidebar-open="false"] > .docs-sidebar {
width: 0;
border-color: transparent;
visibility: hidden;
}
#docs-shell > .docs-sidebar-scrim,
#docs-shell[data-sidebar-open="true"] > .docs-sidebar-scrim {
display: none;
}
}
@media (max-width: 1023px) {
#docs-shell > .docs-sidebar {
position: fixed;
inset-block: 0;
inset-inline-start: 0;
z-index: 50;
width: min(280px, calc(100vw - 48px));
padding-inline-start: env(safe-area-inset-left);
visibility: hidden;
transform: translateX(-100%);
transition:
transform 200ms cubic-bezier(0.22, 1, 0.36, 1),
visibility 0s linear 200ms;
}
#docs-shell[data-sidebar-open="true"] > .docs-sidebar {
visibility: visible;
transform: translateX(0);
transition-delay: 0s;
}
}
.docs-sidebar__header {
display: flex;
flex: none;
align-items: center;
min-height: 44px;
}
.docs-sidebar__nav {
display: flex;
flex: 1;
flex-direction: column;
/* The same rhythm as .sidebar-nav, including its lack of block padding: a
sticky heading cannot cover a band above the scrollport edge, so rows
scrolled visibly through it. */
gap: 1px;
overflow-y: auto;
overscroll-behavior: none;
}
.docs-sidebar__section {
display: flex;
flex-direction: column;
gap: 1px;
}
.docs-sidebar__section-title {
padding-inline: 8px;
color: var(--text-muted);
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.docs-sidebar__link {
padding: 6px 8px;
border-radius: 8px;
/* Same rest-dim/hover-bright rule as .sidebar-row, so the two sidebars in the
app read as one idea rather than two similar ones. */
color: var(--text-muted);
font-size: 0.875rem;
line-height: 1.25rem;
text-decoration: none;
transition: color 150ms ease;
}
@media (hover: hover) {
.docs-sidebar__link:hover {
background: var(--wash-hover);
color: var(--text-primary);
}
}
.docs-main {
flex: 1;
overflow-y: auto;
overscroll-behavior: none;
/* The column is centred in whatever space is left rather than pinned to the
sidebar. Text starting immediately against a rule reads as an overflow of
the sidebar rather than as its own column. */
padding: 40px 64px 96px;
}
.chat-console-composer {
position: relative;
flex: none;
z-index: 30;
border-block-start: 1px solid var(--line-faint);
background: var(--ink-void);
padding: 16px max(16px, env(safe-area-inset-right)) max(16px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
}
.chat-console-composer__form {
width: 100%;
max-width: 46rem;
margin-inline: auto;
}
.chat-console-composer__toolbar {
padding-bottom: 8px;
}
.chat-console-composer__reasoning {
width: auto;
min-height: 28px;
margin: 0;
padding: 3px 28px 3px 8px;
border: 1px solid var(--line-faint);
border-radius: var(--radius-sm);
background-color: var(--ink-void);
color: var(--text-body);
font-size: 0.75rem;
line-height: 1rem;
}
.chat-console-composer__reasoning:hover:not(:disabled),
.chat-console-composer__reasoning:focus-visible {
border-color: var(--line-strong);
color: var(--text-primary);
}
.chat-console-composer__submit {
width: 32px;
height: 32px;
min-height: 32px;
padding: 0;
border-color: var(--line-strong);
border-radius: var(--radius-sm);
background: var(--ink-raised);
color: var(--text-primary);
}
@media (hover: hover) {
.chat-console-composer__submit:hover:not(:disabled) {
border-color: var(--text-muted);
background: var(--ink-hover);
color: var(--text-primary);
}
}
/* Chat UI (OpenAgents) */
/* The transcript and the composer are AI Elements now — `conversation`,
* `message`, `message_content`, and `prompt_input` carry the column, the
* asymmetry, the bubble, and the input group that used to live here as
* `.chat-transcript`, `.message-row`, `.message-body`, `.message-bubble`,
* `.composer`, `.composer-card`, `.composer-grid`, `.composer-input`, and
* `.composer-trailing`. Those rules are gone rather than left to outrank the
* utilities that replaced them: everything in this file is unlayered, so it
* beats every Tailwind layer, and a leftover `background` here would have
* silently repainted the ported bubble.
*
* One declaration survives, because no utility states it: the line breaks a
* person actually typed. */
.message-content {
white-space: pre-wrap;
word-break: break-word;
}
/* The chat composer's send control is `prompt_input_submit/1` now. What is
* left on this class is the voice spike's start control, which is not part of
* the product surface and has no primitive of its own. */
.send-action {
display: inline-flex;
width: 36px;
height: 36px;
flex: none;
align-items: center;
justify-content: center;
border: 0;
border-radius: 50%;
/* The inverse control pair, not `--accent`. The theme went monochrome; this
button kept an indigo fill and was the one saturated element on the
surface. */
background: var(--control-inverse-bg);
color: var(--control-inverse-fg);
cursor: pointer;
transition: filter 150ms ease, transform 150ms ease;
}
.send-action:hover:not(:disabled) {
filter: brightness(1.1);
}
.send-action:active:not(:disabled) {
transform: scale(0.97);
}
/* Theme toggle.
*
* The glyph names the destination, not the current state: this is an action,
* not a status readout. Both glyphs stay in the layout so the control does not
* resize as it flips and shove the breadcrumb beside it.
*
* `data-theme-effective` is set by the head script rather than derived here,
* because with nothing stored the answer lives in the OS, and CSS cannot ask a
* media query whether a preference was *stored*.
*/
@layer components {
/* A circle, not a rounded square: this is an icon-only control, so the
target should be the same shape as the glyph it holds. Width is pinned to
the height so the box is square before the radius rounds it -- padding
alone would make it an oval the moment the glyph changed size. */
/* Selector carries the size attribute deliberately: `.btn[data-size="sm"]`
sets `padding: 8px 15px` at (0,2,0) and beats a bare `.theme-toggle`, so
the padding survived and pushed the glyph off centre inside a 32px box. */
/* Centring is stated here rather than inherited. `.btn[data-size="sm"]` sets
padding at (0,2,0) and outranks a bare class, and the button holds two
glyphs with one hidden, so neither the padding reset nor the flex centring
can be assumed -- both are declared, on a selector that outranks the size
rule. */
.theme-toggle,
.theme-toggle[data-size="sm"] {
display: inline-flex;
width: 32px;
min-width: 32px;
height: 32px;
min-height: 32px;
padding: 0;
gap: 0;
align-items: center;
justify-content: center;
border: 0;
border-radius: 50%;
background: transparent;
color: var(--icon-tertiary);
}
.theme-toggle .icon {
width: 16px;
height: 16px;
font-size: 16px;
}
@media (hover: hover) {
.theme-toggle:hover {
background: var(--wash-hover);
color: var(--icon-primary);
}
}
.theme-toggle__sun,
.theme-toggle__moon {
display: none;
}
/* Dark now, so the control offers light. */
:root[data-theme-effective="dark"] .theme-toggle__sun {
display: inline-flex;
}
/* Light now, so the control offers dark. */
:root[data-theme-effective="light"] .theme-toggle__moon {
display: inline-flex;
}
}