/* 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__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__form { max-width: var(--thread-max-width); margin: 0 auto; display: flex; gap: var(--sp-2); align-items: flex-end; 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__form:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } .composer__input { flex: 1; border: 0; background: none; resize: none; padding: var(--sp-2); font-size: var(--text-base); line-height: var(--leading-normal); max-height: 20rem; } .composer__input:focus { outline: none; } .composer__send { border-radius: var(--radius-full); padding: 0.55rem 0.7rem; } .composer__hint { max-width: var(--thread-max-width); margin: var(--sp-2) auto 0; font-size: var(--text-xs); color: var(--ink-faint); text-align: center; } .select--compact { width: auto; max-width: 16rem; padding: 0.3rem 0.5rem; font-size: var(--text-sm); } /* --- 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; } /* --- Chat settings panel -------------------------------------------------- */ .chat-settings { flex: none; border-bottom: 1px solid var(--border); background: var(--bg-sunken); max-height: 60vh; overflow-y: auto; } .chat-settings__inner { max-width: var(--thread-max-width); margin: 0 auto; padding: var(--sp-4) var(--sp-5); } .chat-settings__note { color: var(--ink-muted); font-size: var(--text-sm); font-style: italic; margin-bottom: var(--sp-4); } .chat-settings__params { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: var(--sp-3); } .chat-settings__params .field { margin-bottom: 0; } /* --- 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. */ :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; }