Files
platform/src/apps/officer-web/styles/prose.css
T
pastilhas 34e21317da chat: the copy buttons sit on the code block, bottom right
they were sticky at the top, which put them over the first line — and on a
one-line block, which is most of them in a chat, over the whole thing.

absolute bottom-right inside the surface now, with `.code-surface` given the
`position: relative` it never had. that omission is why a control could end up
escaping to the nearest positioned ancestor and floating below the block it
belongs to.

`padding-bottom` on the pre to go with the existing `padding-right`: the last
line is the one they overlap now, and it needs its own room the same way the
first line needed it before.

the trade the old comment was worried about is real but smaller than it reads:
a block taller than the viewport now has its controls at the bottom rather
than the top. that is where you end up after reading it anyway, and it is a
worse place to be only for a block you never scroll — which is exactly the
block whose first line the old position was covering.
2026-08-19 23:14:56 +00:00

585 lines
12 KiB
CSS

/* Chat Bubble — Markdown Prose */
.chat-md {
color: hsl(var(--foreground));
font-size: 0.875rem;
line-height: 1.7;
}
.chat-md > *:first-child {
margin-top: 0;
}
.chat-md > *:last-child {
margin-bottom: 0;
}
.chat-md h1 {
font-size: 1.4em;
font-weight: 700;
margin: 1.2em 0 0.4em;
padding-bottom: 0.25em;
border-bottom: 1px solid hsl(var(--border));
letter-spacing: -0.02em;
}
.chat-md h2 {
font-size: 1.2em;
font-weight: 600;
margin: 1.1em 0 0.35em;
padding-bottom: 0.2em;
border-bottom: 1px solid hsl(var(--border));
}
.chat-md h3 {
font-size: 1.1em;
font-weight: 600;
margin: 1em 0 0.3em;
}
.chat-md h4,
.chat-md h5,
.chat-md h6 {
font-size: 1em;
font-weight: 600;
margin: 0.8em 0 0.25em;
}
.chat-md p {
margin: 0.6em 0;
}
.chat-md a {
color: var(--duck-teal);
text-decoration: underline;
text-underline-offset: 2px;
}
.chat-md a:hover {
opacity: 0.8;
}
.chat-md strong {
font-weight: 600;
}
.chat-md blockquote {
margin: 0.75em 0;
padding: 0.4em 0.8em;
border-left: 3px solid var(--duck-teal);
background: color-mix(in srgb, var(--duck-teal) 5%, transparent);
border-radius: 0 0.375rem 0.375rem 0;
color: hsl(var(--muted-foreground));
}
/* Tailwind's preflight resets `list-style: none` on every ul/ol, so a list rendered here had the
indent but no bullets and — more confusingly — no NUMBERS: an ordered list read as an unordered
pile of lines, and copying the same text elsewhere showed it numbered correctly, which makes it
look like the model emitted bad markdown rather than the stylesheet eating the markers. The
`li::marker` rule below was colouring a marker that was never drawn. */
.chat-md ul,
.chat-md ol {
margin: 0.5em 0;
padding-left: 1.5em;
list-style-position: outside;
}
.chat-md ul {
list-style-type: disc;
}
.chat-md ol {
list-style-type: decimal;
}
/* Nested levels, following the usual typographic convention so depth stays readable. */
.chat-md ul ul {
list-style-type: circle;
}
.chat-md ul ul ul {
list-style-type: square;
}
.chat-md ol ol {
list-style-type: lower-alpha;
}
.chat-md ol ol ol {
list-style-type: lower-roman;
}
/* A task list carries its own checkbox; a bullet next to it is noise. */
.chat-md li:has(> input[type='checkbox']) {
list-style-type: none;
}
.chat-md li {
margin: 0.2em 0;
}
.chat-md li::marker {
color: var(--duck-teal);
}
.chat-md code {
padding: 0.15em 0.4em;
border-radius: 0.25rem;
background: color-mix(in srgb, var(--duck-teal) 10%, transparent);
color: var(--duck-teal);
font-size: 0.85em;
font-family: ui-monospace, monospace;
}
.chat-md pre {
margin: 0.75em 0;
padding: 0.75em 1em;
border-radius: 0.5rem;
background: #0d1117;
overflow-x: auto;
font-size: 0.8125rem;
line-height: 1.6;
}
.chat-md pre code {
padding: 0;
border-radius: 0;
background: none;
color: #e6edf3;
font-size: inherit;
}
.chat-md hr {
border: none;
height: 1px;
background: hsl(var(--border));
margin: 1.5em 0;
}
/* ── The shared code surface (`CodeSurface`) ──
Deliberately NOT scoped under `.chat-md`: a tool call renders code too, and it lives outside the prose
container. These rules come after the `.chat-md pre` block above so they win the specificity tie when a
surface does sit inside a chat reply. */
/* Vertical rhythm belongs to the prose that surrounds a block, not to the block. Inside a tool panel the
surface is already boxed and spaced by its container, and a leaked 0.75em reads as a gap nobody chose. */
.chat-md .code-surface {
margin: 0.75em 0;
}
.code-surface pre {
margin: 0;
padding: 0.75em 1em;
/* Room for the copy controls, which sit absolutely at the bottom-right of the block. Horizontal so a
long line scrolls past them rather than under them; vertical so the LAST line — the one they now
overlap — keeps its own space. */
padding-right: 5.5em;
padding-bottom: 2em;
border-radius: 0.5rem;
background: #0d1117;
/* Scroll, never wrap: wrapping folds `ls -la`, `git log --oneline` and any other columnar output into
mush, and it breaks the one-line-per-number contract the gutter makes. */
overflow-x: auto;
white-space: pre;
font-size: 0.8125rem;
line-height: 1.6;
}
/* A block that named a language also shows the name, which needs its own room. */
.code-surface[data-lang] pre {
padding-right: 10em;
}
/* The positioning context for those controls. Without it they escape to the nearest positioned ancestor,
which is how a copy button ends up floating below the block it belongs to. */
.code-surface {
position: relative;
}
.code-surface pre code {
padding: 0;
border-radius: 0;
background: none;
color: #e6edf3;
font-size: inherit;
font-family: ui-monospace, monospace;
}
/* Line numbers come from a counter rather than from markup, because generated content is not part of
`textContent` — so the copy buttons, and a hand-made selection, still yield the code and nothing else.
Both branches of the block emit `.line` spans, so the numbers survive the swap to highlighted output.
`--line-offset` seeds the counter: a `Read` of lines 400-460 has to say 400, not 1.
They scroll with a long line rather than sticking to the left edge; a sticky gutter would need an
opaque background, and that means hardcoding a colour the theme is entitled to change. */
.code-surface[data-numbered] code {
counter-reset: line var(--line-offset, 0);
}
.code-surface[data-numbered] .line::before {
counter-increment: line;
content: counter(line);
display: inline-block;
width: calc(var(--line-digits, 2) * 1ch);
margin-right: 1.25em;
text-align: right;
color: #6e7681;
user-select: none;
-webkit-user-select: none;
}
.chat-md table {
width: 100%;
border-collapse: collapse;
margin: 0.75em 0;
font-size: 0.85em;
}
.chat-md th {
color: hsl(var(--muted-foreground));
background: color-mix(in srgb, var(--duck-teal) 8%, transparent);
font-weight: 600;
text-align: left;
padding: 0.4em 0.6em;
border: 1px solid hsl(var(--border));
}
.chat-md td {
padding: 0.4em 0.6em;
border: 1px solid hsl(var(--border));
}
.chat-md tr:nth-child(even) {
background: hsl(var(--muted) / 0.3);
}
.chat-md img {
max-width: 100%;
border-radius: 0.375rem;
margin: 0.75em 0;
}
.chat-md input[type='checkbox'] {
accent-color: var(--duck-teal);
margin-right: 0.4em;
}
/* File Viewer — Markdown Prose */
.file-viewer-md {
color: hsl(var(--foreground));
font-size: 0.95rem;
line-height: 1.75;
}
.file-viewer-md h1 {
color: hsl(var(--foreground));
font-size: 2em;
font-weight: 700;
margin: 1.5em 0 0.5em;
padding-bottom: 0.3em;
border-bottom: 2px solid hsl(var(--border));
letter-spacing: -0.02em;
}
.file-viewer-md h2 {
color: hsl(var(--foreground));
font-size: 1.5em;
font-weight: 600;
margin: 1.4em 0 0.4em;
padding-bottom: 0.25em;
border-bottom: 1px solid hsl(var(--border));
}
.file-viewer-md h3 {
color: hsl(var(--foreground));
font-size: 1.25em;
font-weight: 600;
margin: 1.2em 0 0.4em;
}
.file-viewer-md h4,
.file-viewer-md h5,
.file-viewer-md h6 {
color: hsl(var(--foreground));
font-size: 1.05em;
font-weight: 600;
margin: 1em 0 0.3em;
}
.file-viewer-md p {
margin: 0.75em 0;
}
.file-viewer-md a {
color: var(--duck-teal);
text-decoration: underline;
text-underline-offset: 2px;
}
.file-viewer-md a:hover {
opacity: 0.8;
}
.file-viewer-md strong {
font-weight: 600;
}
.file-viewer-md blockquote {
margin: 1em 0;
padding: 0.5em 1em;
border-left: 3px solid var(--duck-teal);
background: color-mix(in srgb, var(--duck-teal) 5%, transparent);
border-radius: 0 0.5rem 0.5rem 0;
color: hsl(var(--muted-foreground));
}
.file-viewer-md ul,
.file-viewer-md ol {
margin: 0.75em 0;
padding-left: 1.75em;
list-style-position: outside;
}
.file-viewer-md ul {
list-style-type: disc;
}
.file-viewer-md ol {
list-style-type: decimal;
}
.file-viewer-md ul ul {
list-style-type: circle;
}
.file-viewer-md ol ol {
list-style-type: lower-alpha;
}
.file-viewer-md li:has(> input[type='checkbox']) {
list-style-type: none;
}
.file-viewer-md li {
margin: 0.25em 0;
}
.file-viewer-md li::marker {
color: var(--duck-teal);
}
.file-viewer-md hr {
border: none;
height: 1px;
background: hsl(var(--border));
margin: 2em 0;
}
.file-viewer-md table {
width: 100%;
border-collapse: collapse;
margin: 1em 0;
font-size: 0.9em;
}
.file-viewer-md th {
color: hsl(var(--muted-foreground));
background: color-mix(in srgb, var(--duck-teal) 8%, transparent);
font-weight: 600;
text-align: left;
padding: 0.5em 0.75em;
border: 1px solid hsl(var(--border));
}
.file-viewer-md td {
padding: 0.5em 0.75em;
border: 1px solid hsl(var(--border));
}
.file-viewer-md tr:nth-child(even) {
background: hsl(var(--muted) / 0.3);
}
.file-viewer-md img {
max-width: 100%;
border-radius: 0.5rem;
margin: 1em 0;
}
.file-viewer-md input[type='checkbox'] {
accent-color: var(--duck-teal);
margin-right: 0.5em;
}
/* Skills — Markdown Prose */
.skill-md {
color: hsl(var(--foreground));
font-size: 0.95rem;
line-height: 1.75;
}
.skill-md h1 {
color: hsl(var(--foreground));
font-size: 2em;
font-weight: 700;
margin: 1.5em 0 0.5em;
padding-bottom: 0.3em;
border-bottom: 2px solid hsl(var(--border));
letter-spacing: -0.02em;
}
.skill-md h2 {
color: hsl(var(--foreground));
font-size: 1.5em;
font-weight: 600;
margin: 1.4em 0 0.4em;
padding-bottom: 0.25em;
border-bottom: 1px solid hsl(var(--border));
}
.skill-md h3 {
color: hsl(var(--foreground));
font-size: 1.25em;
font-weight: 600;
margin: 1.2em 0 0.4em;
}
.skill-md h4,
.skill-md h5,
.skill-md h6 {
color: hsl(var(--foreground));
font-size: 1.05em;
font-weight: 600;
margin: 1em 0 0.3em;
}
.skill-md p {
margin: 0.75em 0;
}
.skill-md a {
color: var(--duck-teal);
text-decoration: underline;
text-underline-offset: 2px;
}
.skill-md a:hover {
opacity: 0.8;
}
.skill-md strong {
font-weight: 600;
}
.skill-md blockquote {
margin: 1em 0;
padding: 0.5em 1em;
border-left: 3px solid var(--duck-teal);
background: color-mix(in srgb, var(--duck-teal) 5%, transparent);
border-radius: 0 0.5rem 0.5rem 0;
color: hsl(var(--muted-foreground));
}
.skill-md ul,
.skill-md ol {
margin: 0.75em 0;
padding-left: 1.75em;
list-style-position: outside;
}
.skill-md ul {
list-style-type: disc;
}
.skill-md ol {
list-style-type: decimal;
}
.skill-md ul ul {
list-style-type: circle;
}
.skill-md ol ol {
list-style-type: lower-alpha;
}
.skill-md li:has(> input[type='checkbox']) {
list-style-type: none;
}
.skill-md li {
margin: 0.25em 0;
}
.skill-md li::marker {
color: var(--duck-teal);
}
.skill-md code {
padding: 0.15em 0.4em;
border-radius: 0.25rem;
background: color-mix(in srgb, var(--duck-teal) 10%, transparent);
color: var(--duck-teal);
font-size: 0.85em;
font-family: ui-monospace, monospace;
}
.skill-md pre {
margin: 1em 0;
padding: 1em;
border-radius: 0.5rem;
background: #0d1117;
overflow-x: auto;
font-size: 0.875rem;
line-height: 1.6;
}
.skill-md pre code {
padding: 0;
border-radius: 0;
background: none;
color: #e6edf3;
font-size: inherit;
}
.skill-md hr {
border: none;
height: 1px;
background: hsl(var(--border));
margin: 2em 0;
}
.skill-md table {
width: 100%;
border-collapse: collapse;
margin: 1em 0;
font-size: 0.9em;
}
.skill-md th {
color: hsl(var(--muted-foreground));
background: color-mix(in srgb, var(--duck-teal) 8%, transparent);
font-weight: 600;
text-align: left;
padding: 0.5em 0.75em;
border: 1px solid hsl(var(--border));
}
.skill-md td {
padding: 0.5em 0.75em;
border: 1px solid hsl(var(--border));
}
.skill-md tr:nth-child(even) {
background: hsl(var(--muted) / 0.3);
}
.skill-md img {
max-width: 100%;
border-radius: 0.5rem;
margin: 1em 0;
}
.skill-md input[type='checkbox'] {
accent-color: var(--duck-teal);
margin-right: 0.5em;
}