/* Chat thread, composer, message bodies and code blocks. Loaded only on chat pages. Like app.css, every value resolves through tokens.css. */ /* --- Thread --------------------------------------------------------------- */ .thread-scroll { flex: 1; overflow-y: auto; scroll-behavior: smooth; scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; } .thread { max-width: var(--thread-max-width); margin: 0 auto; padding: var(--sp-6) var(--sp-5) var(--sp-8); display: flex; flex-direction: column; gap: var(--sp-6); } .thread__intro { display: grid; place-items: center; gap: var(--sp-3); text-align: center; padding: var(--sp-12) 0 var(--sp-6); } /* --- Messages ------------------------------------------------------------- */ .msg { display: grid; grid-template-columns: 2rem 1fr; gap: var(--sp-3); align-items: start; } .msg__gutter { display: grid; place-items: center; width: 2rem; height: 2rem; border-radius: var(--radius-full); overflow: hidden; } .msg__mark { width: 2rem; height: 2rem; } .msg__avatar { width: 2rem; height: 2rem; border-radius: var(--radius); } .msg__initial { width: 2rem; height: 2rem; display: grid; place-items: center; border-radius: var(--radius-full); background: var(--surface-active); color: var(--ink-muted); font-size: var(--text-sm); font-weight: 600; } .msg__main { min-width: 0; } .msg__meta { display: flex; align-items: baseline; gap: var(--sp-2); margin-bottom: var(--sp-1); } .msg__author { font-family: var(--font-display); font-size: var(--text-base); font-weight: 600; } .msg__model { font-size: var(--text-xs); color: var(--ink-faint); font-family: var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 16rem; } .msg__body { line-height: var(--leading-relaxed); overflow-wrap: break-word; } /* User turns and mid-stream assistant text are plain text, so newlines and runs of spaces have to survive. */ .msg__body--plain, .msg__body--streaming { white-space: pre-wrap; } .msg--user .msg__body--plain { background: var(--bubble-user); padding: var(--sp-3) var(--sp-4); border-radius: var(--radius-lg); display: inline-block; max-width: 100%; } .msg__error { margin: var(--sp-2) 0; align-items: flex-start; } /* --- Streaming indicator -------------------------------------------------- */ /* Shown until the first token arrives, then hidden by the sibling selector below -- no JavaScript involved in either direction. */ .msg__waiting { padding: var(--sp-2) 0; } .msg__body--streaming:not(:empty) + .msg__waiting { display: none; } .dots { display: inline-flex; gap: 0.25rem; align-items: center; } .dots i { width: 0.4rem; height: 0.4rem; border-radius: var(--radius-full); background: var(--ink-faint); animation: dot-pulse 1.3s ease-in-out infinite; } .dots i:nth-child(2) { animation-delay: 0.18s; } .dots i:nth-child(3) { animation-delay: 0.36s; } @keyframes dot-pulse { 0%, 60%, 100% { opacity: 0.28; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-2px); } } /* A caret trailing the text while it streams. */ .msg__body--streaming::after { content: ""; display: inline-block; width: 0.45rem; height: 1.05em; margin-left: 1px; vertical-align: text-bottom; background: var(--gold); opacity: 0.75; animation: caret 1.05s steps(1) infinite; } @keyframes caret { 0%, 49% { opacity: 0.75; } 50%, 100% { opacity: 0; } } /* --- Reasoning ------------------------------------------------------------ */ .reasoning { margin: 0 0 var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius); background: color-mix(in srgb, var(--surface) 70%, transparent); font-size: var(--text-sm); } /* A live block is emitted before the first token arrives, and plenty of models emit no reasoning at all. Hiding it until it has content means those models never show an empty "Thinking" box, and no JavaScript is involved either way. */ .reasoning--live:not(:has(.reasoning__body:not(:empty))) { display: none; } .reasoning__summary { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); cursor: pointer; color: var(--ink-muted); list-style: none; user-select: none; border-radius: var(--radius); } .reasoning__summary::-webkit-details-marker { display: none; } .reasoning__summary:hover { color: var(--ink); background: var(--surface-hover); } .reasoning__icon { color: var(--gold); flex: none; } .reasoning__label { flex: 1; font-style: italic; } .reasoning__chevron { flex: none; transition: transform var(--transition-fast); } .reasoning[open] .reasoning__chevron { transform: rotate(180deg); } .reasoning__body { padding: 0 var(--sp-3) var(--sp-3); margin-left: var(--sp-2); border-left: 2px solid var(--border-strong); padding-left: var(--sp-3); white-space: pre-wrap; color: var(--ink-muted); font-size: var(--text-sm); line-height: var(--leading-relaxed); max-height: 26rem; overflow-y: auto; scrollbar-width: thin; } /* Gentle pulse on the icon while thinking is still streaming. */ .reasoning--live .reasoning__icon { animation: think-pulse 1.6s ease-in-out infinite; } @keyframes think-pulse { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } } /* --- Message actions ------------------------------------------------------ */ .msg__actions { display: flex; gap: var(--sp-1); margin-top: var(--sp-2); opacity: 0; transition: opacity var(--transition-fast); } .msg:hover .msg__actions, .msg:focus-within .msg__actions { opacity: 1; } .msg__actions .is-copied { color: var(--success); } /* --- Rendered Markdown ---------------------------------------------------- */ .msg__body > :first-child { margin-top: 0; } .msg__body > :last-child { margin-bottom: 0; } .msg__body h1, .msg__body h2, .msg__body h3, .msg__body h4, .msg__body h5, .msg__body h6 { margin: var(--sp-5) 0 var(--sp-2); } .msg__body h1 { font-size: var(--text-xl); } .msg__body h2 { font-size: var(--text-lg); } .msg__body h3 { font-size: var(--text-md); } .msg__body ul, .msg__body ol { margin: 0 0 var(--sp-4); padding-left: var(--sp-6); } .msg__body li { margin-bottom: var(--sp-1); } .msg__body blockquote { margin: 0 0 var(--sp-4); padding: var(--sp-1) var(--sp-4); border-left: 3px solid var(--border-strong); color: var(--ink-muted); font-style: italic; } .msg__body hr { border: 0; border-top: 1px solid var(--border); margin: var(--sp-5) 0; } .msg__body :not(pre) > code { font-family: var(--font-mono); font-size: 0.875em; padding: 0.13em 0.36em; border-radius: var(--radius-sm); background: var(--code-bg); border: 1px solid var(--code-border); } .msg__body table { width: 100%; border-collapse: collapse; margin: 0 0 var(--sp-4); font-size: var(--text-sm); display: block; overflow-x: auto; } .msg__body th, .msg__body td { border: 1px solid var(--border); padding: var(--sp-2) var(--sp-3); text-align: left; } .msg__body th { background: var(--surface); font-weight: 600; } .msg__body img { max-width: 100%; height: auto; border-radius: var(--radius); } /* --- Code blocks ---------------------------------------------------------- */ .code-block { margin: 0 0 var(--sp-4); border: 1px solid var(--code-border); border-radius: var(--radius); background: var(--code-bg); overflow: hidden; } .code-block__label { padding: var(--sp-1) var(--sp-3); font-family: var(--font-mono); font-size: var(--text-xs); color: var(--ink-faint); border-bottom: 1px solid var(--code-border); background: color-mix(in srgb, var(--code-bg) 60%, var(--surface)); } .code-block__pre { margin: 0; padding: var(--sp-3) var(--sp-4); overflow-x: auto; font-family: var(--font-mono); font-size: var(--text-sm); line-height: 1.55; } .code-block__pre code { font-family: inherit; background: none; border: 0; padding: 0; } /* Pygments token colours, mapped onto theme tokens rather than a fixed scheme, so code follows the active theme. classprefix "pg-" is set in markdown.py. */ .pg-c, .pg-c1, .pg-cm, .pg-cs, .pg-cp { color: var(--ink-faint); font-style: italic; } .pg-k, .pg-kn, .pg-kd, .pg-kc, .pg-kr, .pg-kt { color: var(--gold); } .pg-s, .pg-s1, .pg-s2, .pg-sb, .pg-sd, .pg-se, .pg-sh, .pg-si, .pg-sx { color: var(--success); } .pg-m, .pg-mi, .pg-mf, .pg-mh, .pg-mo { color: var(--danger); } .pg-nf, .pg-nd { color: var(--accent); } .pg-nc, .pg-nn { color: var(--accent-hover); font-weight: 600; } .pg-nb, .pg-bp { color: var(--accent); } .pg-nv, .pg-vi, .pg-vg, .pg-vc { color: var(--ink); } .pg-o, .pg-ow, .pg-p { color: var(--ink-muted); } .pg-err { color: var(--danger); } .pg-gd { color: var(--danger); } .pg-gi { color: var(--success); } /* --- Composer ------------------------------------------------------------- */ .composer { flex: none; padding: var(--sp-3) var(--sp-5) var(--sp-4); border-top: 1px solid var(--border); background: var(--bg); } .composer__inner { max-width: var(--thread-max-width); margin: 0 auto; } /* A column: chips on top, then the control row. The chips are inside the form so their hidden file_ids inputs are submitted with the message. */ .composer__form { display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-2); border: 1px solid var(--border); border-radius: var(--radius-xl); background: var(--surface); transition: border-color var(--transition-fast), box-shadow var(--transition-fast); } .composer__row { display: flex; gap: var(--sp-1); align-items: flex-end; } /* Attach and send are the same size and sit on the same baseline as the last line of the textarea, so the control row reads as one object. */ .composer__btn { flex: none; align-self: flex-end; border-radius: var(--radius-full); } .composer__form:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } .composer__input { flex: 1; min-width: 0; border: 0; background: none; resize: none; padding: 0.5rem var(--sp-2); font-size: var(--text-base); line-height: var(--leading-normal); max-height: 20rem; color: var(--ink); } .composer__input:focus { outline: none; } .composer__hint { margin: var(--sp-2) 0 0; font-size: var(--text-xs); color: var(--ink-faint); text-align: center; } /* --- Folders -------------------------------------------------------------- */ .folder__row { padding-right: var(--sp-1); } .folder__toggle { display: flex; align-items: center; gap: var(--sp-2); flex: 1; min-width: 0; background: none; border: 0; padding: 0; color: inherit; font: inherit; cursor: pointer; text-align: left; } .folder__chevron { display: inline-flex; transition: transform var(--transition-fast); } .folder__chevron.is-open { transform: rotate(90deg); } .folder__contents { padding-left: var(--sp-4); } .nav-item__link { display: flex; align-items: center; gap: var(--sp-2); flex: 1; min-width: 0; color: inherit; text-decoration: none; } /* --- Attachment chips (composer) ------------------------------------------ */ .composer { position: relative; } .composer__attachments { display: flex; flex-wrap: wrap; gap: var(--sp-2); padding: var(--sp-1) var(--sp-1) 0; } .composer__attachments:empty { display: none; } .chip { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2); border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); max-width: 20rem; font-size: var(--text-sm); } .chip--error { border-color: var(--danger); background: var(--danger-soft); } .chip--error .chip__icon { color: var(--danger); } .chip__thumb { width: 2.25rem; height: 2.25rem; border-radius: var(--radius-sm); object-fit: cover; flex: none; } .chip__icon { color: var(--ink-muted); flex: none; display: flex; } .chip__body { min-width: 0; flex: 1; display: flex; flex-direction: column; } .chip__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .chip__meta { font-size: var(--text-xs); color: var(--ink-faint); } .chip__warning { font-size: var(--text-xs); color: var(--danger); } /* --- Drag and drop -------------------------------------------------------- */ .dropzone-overlay { position: absolute; inset: 0; z-index: 5; display: none; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-2); border: 2px dashed var(--accent); border-radius: var(--radius-lg); background: color-mix(in srgb, var(--bg) 88%, var(--accent)); color: var(--accent); font-weight: 500; /* The overlay must not eat the drop event it is advertising. */ pointer-events: none; } .composer.is-dropping .dropzone-overlay { display: flex; } /* --- Attachments in the thread -------------------------------------------- */ .attachments { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-2); } .attachments__image { display: block; border-radius: var(--radius); overflow: hidden; border: 1px solid var(--border); line-height: 0; } .attachments__image img { max-width: min(22rem, 100%); max-height: 20rem; width: auto; height: auto; object-fit: contain; } .attachments__doc { display: flex; align-items: flex-start; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); font-size: var(--text-sm); max-width: 100%; } .attachments__doc-body { display: flex; flex-direction: column; min-width: 0; } .attachments__doc-body > a { overflow-wrap: anywhere; } /* --- Model avatars -------------------------------------------------------- */ .model-avatar { width: 2rem; height: 2rem; flex: none; border-radius: var(--radius); object-fit: cover; background: var(--surface-active); } .model-avatar--initial { display: grid; place-items: center; font-family: var(--font-display); font-weight: 600; font-size: var(--text-sm); /* Hue comes from the model id (see the stable_hue filter); saturation and lightness are fixed so every generated badge stays legible in both themes. */ background: hsl(var(--avatar-hue, 40) 42% 34%); color: hsl(var(--avatar-hue, 40) 60% 92%); } :root[data-theme="shire"] .model-avatar--initial { background: hsl(var(--avatar-hue, 40) 46% 82%); color: hsl(var(--avatar-hue, 40) 60% 22%); } /* --- Theme toggle --------------------------------------------------------- */ /* Only the icon for the theme you would switch TO is shown. Both live in the same button, so the wrapper must not add a line box of its own. */ .theme-icon { display: flex; } :root[data-theme="moria"] .theme-icon--dark { display: none; } :root[data-theme="shire"] .theme-icon--light { display: none; } /* Alpine sets x-cloak until it has initialised; without this, collapsed folders flash open on every page load. */ [x-cloak] { display: none !important; }