@use 'kit-deps' as *;
@use 'tokens' as *;
// ─── Input bar ─────────────────────────────────────────────────────────────
.input-bar {
flex-shrink: 0;
padding: 8px 15px;
background: $surface-page;
border-top: $border-width-base $border-style-base $border-color-muted;
@media (max-width: 767px) {
padding: 0; /* empty bar is flush; the attachment strip below owns the gaps */
}
}
.input-dropzone {
position: relative;
&.is-dragging::after {
content: 'Drop files here';
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba($color-secondary, 0.12);
border: 2px dashed $color-secondary;
font-size: 14px;
color: $color-secondary;
pointer-events: none;
z-index: 10;
}
}
.file-preview-strip {
display: flex;
flex-wrap: wrap;
gap: $space-xs;
margin-bottom: $space-xs;
max-width: 920px;
margin-left: auto;
margin-right: auto;
@media (max-width: 767px) {
/* The strip, not the bar, provides the top gap on full-bleed mobile */
padding-top: $space-sm;
}
}
.file-preview-image {
position: relative;
width: 60px;
height: 60px;
img {
width: 100%;
height: 100%;
object-fit: cover;
border: $border-width-base $border-style-base $border-color-muted;
}
}
.file-preview-pill.file-preview-pill {
/* GnChip root: filenames must not inherit the kit chip's uppercase */
text-transform: none;
max-width: 180px;
/* Inside the kit's inline-flex chip the name span won't shrink below its
content unless min-width is lifted (default `auto`), so the ellipsis
only kicks in with an explicit min-width: 0 */
.file-preview-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
/* Thumb stays app-owned (no kit image-preview component) — kit surface look */
.file-preview-image {
border: $border-width-base $border-style-base $border-color-muted;
border-radius: $border-radius-sm;
overflow: hidden;
background: $surface-panel-muted;
}
.file-preview-remove {
background: none;
border: none;
cursor: pointer;
color: $color-text-dark;
padding: 0;
display: flex;
align-items: center;
font-size: $icon-size-xs;
transition: color $motion-fast $motion-ease;
flex-shrink: 0;
&:hover { color: $color-error; }
}
.upload-progress {
height: 2px;
background: $surface-panel-strong;
margin-bottom: $space-xs;
overflow: hidden;
}
.upload-progress-fill {
height: 100%;
background: $color-secondary;
transition: width $motion-base $motion-ease;
}
// Indeterminate variant: fetch() gives no progress fraction, so the fill
// slides across instead of sitting at a fake fixed percentage.
.upload-progress.is-indeterminate {
.upload-progress-fill {
width: 30%;
transition: none;
animation: upload-slide 1.1s ease-in-out infinite;
}
}
@keyframes upload-slide {
0% { transform: translateX(-110%); }
100% { transform: translateX(440%); }
}
.input-row {
display: flex;
align-items: center;
gap: $space-sm;
background: $surface-panel-muted;
border: $border-width-base $border-style-base $color-text-light;
border-radius: $border-radius-sm;
padding: $space-sm;
max-width: 920px;
margin: 0 auto;
&:hover {
border-bottom-color: $color-text-dark;
}
&:focus-within {
border-color: $color-electric-blue;
background: transparent;
}
@media (max-width: 767px) {
border: none;
border-radius: 0;
max-width: none;
padding: $space-sm $space-md;
}
}
.input-textarea {
flex: 1;
background: none;
border: none;
outline: none;
resize: none;
color: $color-text-light;
font-size: 14px;
line-height: 1.5;
font-family: inherit;
min-height: 24px;
max-height: 144px;
overflow-y: auto;
padding: 2px 0;
&::placeholder { color: $color-text-dark; }
}
// ─── Welcome screen ────────────────────────────────────────────────────────
.welcome-screen {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
// `safe center` is not cosmetic: with a plain `center`, content taller than
// the pane overflows past the flex start edge and cannot be scrolled back to.
// The profile grid grows with the profile list, so this pane must be able to
// scroll — on a phone it is taller than the viewport with more than a few.
justify-content: safe center;
overflow-y: auto;
padding: $space-xl;
gap: $space-xl;
text-align: center;
@media (max-width: 768px) {
padding: 15px;
gap: $space-md;
justify-content: flex-start;
padding-top: 52px;
}
}
.welcome-logo {
display: flex;
flex-direction: column;
align-items: center;
gap: $space-sm;
img { width: 64px; height: 64px; }
h1 {
font-size: 30px; // kit h1 (typography tokens are not exported by kit-deps)
font-weight: 600;
color: $color-text-light;
margin: 0;
}
p {
font-size: var(--gn-font-size-base);
color: $color-text-medium;
margin: 0;
}
@media (max-width: 768px) {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: $space-sm;
img { width: 36px; height: 36px; }
h1 { font-size: 22px; }
p {
font-size: 13px;
flex-basis: 100%;
}
}
}
.welcome-profiles {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
// `1fr` rows all take the height of the tallest one, so a card whose name
// wraps or whose description runs long does not stand out. A fixed pixel
// height would not survive that: "Server Administrator" wraps on a phone.
grid-auto-rows: 1fr;
// The pane scrolls, so the grid must not be squeezed to fit it.
flex-shrink: 0;
gap: $space-md;
width: min(100%, 880px);
@media (max-width: 768px) {
grid-template-columns: repeat(2, 1fr);
gap: 10px;
width: 100%;
max-width: 100%;
}
}
.welcome-profile-card {
display: flex;
flex-direction: column;
align-items: center;
gap: $space-sm;
padding: $space-md;
background: $surface-panel-muted;
border: $border-width-base $border-style-base $border-color-muted;
border-radius: $border-radius-md;
cursor: pointer;
transition: border-color $motion-fast $motion-ease, background $motion-fast $motion-ease;
width: 100%;
text-align: center;
&:hover {
border-color: $color-secondary;
background: $surface-panel-strong;
}
&.is-creating {
opacity: 0.6;
pointer-events: none;
}
.profile-name {
font-size: 14px;
font-weight: 600;
color: $color-text-light;
}
.profile-desc {
font-size: 12px;
color: $color-text-dark;
line-height: 1.4;
// No line clamp: a description the user cannot finish reading is worse
// than a taller card, and `grid-auto-rows: 1fr` keeps the row even anyway.
}
@media (max-width: 768px) {
width: auto;
padding: 10px;
gap: 6px;
.profile-name { font-size: 13px; }
.profile-desc { font-size: 11px; }
}
}
// ─── Empty state ───────────────────────────────────────────────────────────
// Centers the GnEmptyState card inside the chat area.
.empty-chat {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
// ─── Spinner ───────────────────────────────────────────────────────────────
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid $border-color-muted;
border-top-color: $color-secondary;
border-radius: 50%;
animation: spin 0.6s linear infinite;
flex-shrink: 0;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
// ─── Animations ────────────────────────────────────────────────────────────
.fade-enter-active,
.fade-leave-active {
transition: opacity $motion-fast $motion-ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.msg-enter {
animation: msgEnter $motion-base $motion-ease both;
}
@keyframes msgEnter {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.slide-left-enter-active,
.slide-left-leave-active {
transition: transform $motion-slow $motion-ease, opacity $motion-slow $motion-ease;
}
.slide-left-enter-from {
transform: translateX(-100%);
opacity: 0;
}
.slide-left-leave-to {
transform: translateX(-100%);
opacity: 0;
}
// ─── Confirm dialog transition ─────────────────────────────────────────────
.modal-enter-active,
.modal-leave-active {
transition: opacity $motion-slow $motion-ease;
.modal-panel {
transition: opacity $motion-slow $motion-ease, margin-top $motion-slow $motion-ease;
}
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
.modal-panel {
opacity: 0;
margin-top: 200px;
}
}
// ─── Disclosure chevrons ───────────────────────────────────────────────────
// Kit 1.0 dropped the hover icon rotation entirely; app-owned
// tool/thinking cards keep the open-state chevron flip.
.tool-card[open] > summary .tool-chevron,
.thinking-card[open] > summary .tool-chevron {
transform: rotate(180deg) !important;
}
// ─── Syntax highlighting ───────────────────────────────────────────────────
// highlight.js emits `.hljs-*` classes but ships no theme of its own — without
// these rules every highlighted block renders in one flat text colour, which
// is what an un-styled `hljs` looks like. One palette, declared once, so the
// message cards, the artifact panel and the content lightbox all agree.
// Colours are kit palette tokens, picked for the dark default theme.
.hljs {
background: transparent;
color: inherit;
}
.hljs-comment,
.hljs-quote {
color: $color-text-dark;
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-doctag,
.hljs-formula {
color: $color-purple;
}
.hljs-string,
.hljs-regexp,
.hljs-addition,
.hljs-char.escape_ {
color: $color-neon-green;
}
.hljs-number,
.hljs-symbol,
.hljs-bullet,
.hljs-link {
color: $color-orange;
}
.hljs-title,
.hljs-title.class_,
.hljs-title.function_,
.hljs-section,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: $color-electric-blue;
}
.hljs-attr,
.hljs-attribute,
.hljs-variable,
.hljs-template-variable,
.hljs-property {
color: $color-cyan;
}
.hljs-type,
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-params,
.hljs-operator,
.hljs-punctuation {
color: $color-neon-yellow;
}
.hljs-meta,
.hljs-meta .hljs-keyword {
color: $color-teal;
}
.hljs-deletion {
color: $color-error;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 600;
}
// ─── Search result message flash ────────────────────────────────────────────
.msg-flash {
animation: msgFlash 2.5s ease-out;
}
@keyframes msgFlash {
0% { filter: brightness(1); }
15% { filter: brightness(1.4); background-color: rgba($color-secondary, 0.12); }
40% { filter: brightness(1.4); background-color: rgba($color-secondary, 0.12); }
100% { filter: brightness(1); }
}