c8d715346f
The report was "a narrow window clips grid columns without usable horizontal
scroll", and the ticket suspected a flex `min-width: auto` trap swallowing
`#grid`'s overflow. It was not that: `#grid` has `overflow: auto`, which
already zeroes a flex item's automatic minimum. What could not shrink was the
app bar.
Measured at 22ab58f on the grouped harness fixture, `documentElement.scrollWidth`
read 1011 at every window narrower than 1011px — a floor, not a slope, which is
the signature of a sum of `flex: none` reserves. Below it the PAGE grew a
horizontal scrollbar, and what it scrolled to was the bar's own right-hand end
sitting outside the window, while the grid slid off to the left.
`header` takes `flex-wrap: wrap`, chosen over a `@media` breakpoint because a
flex item wraps exactly when the line cannot hold it, so the desktop bar is
untouched by construction rather than by a number someone picked. `main` gets
the `min-width: 0` the ticket asked about, and the three side panels are capped
at 40vw so the grid keeps the larger share of a phone window.
What this deliberately does NOT do: make `#grid` scroll to columns beyond the
announced extent. The browser's Viewport is a real window (gotcha 44/47), so
those columns are never projected and no scrolling can reveal them; the
keyboard already reaches every one, and a spec now asserts that.
`harness/tests/narrow.spec.js` asserts `scrollWidth <= clientWidth`, which is a
question a browser answers exactly. `body { overflow-x: hidden }` was rejected
for making that assertion vacuous.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
787 lines
20 KiB
CSS
787 lines
20 KiB
CSS
/* Paint only. Every fact this file colours in — which cell is selected, which
|
|
axis a category is on, whether a cell may be typed into, what the prompt
|
|
says — is decided in Rust and arrives already decided. Nothing here derives
|
|
anything; see context/design-principles.md §5 and context/code/clients.md.
|
|
|
|
The palette is deliberately the landing page's, so docs/index.html and this
|
|
application read as one product rather than two. */
|
|
|
|
:root {
|
|
--bg: #fbfaf7;
|
|
--surface: #f4f1ea;
|
|
--raised: #ffffff;
|
|
--fg: #1c1b19;
|
|
--dim: #6b675f;
|
|
--line: #ddd8ce;
|
|
--line-strong: #bdb6a8;
|
|
--accent: #2f6f4f;
|
|
--accent-soft: #e2efe6;
|
|
--accent-ink: #ffffff;
|
|
--selected: #e8e2d4;
|
|
--error: #a8322d;
|
|
--shadow: 0 10px 30px -12px rgb(28 27 25 / 35%);
|
|
--radius: 5px;
|
|
color-scheme: light dark;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--bg: #14161a;
|
|
--surface: #1b1e24;
|
|
--raised: #22262e;
|
|
--fg: #d7dae0;
|
|
--dim: #8b93a1;
|
|
--line: #2b2f37;
|
|
--line-strong: #414753;
|
|
--accent: #8ec07c;
|
|
--accent-soft: #1f2a22;
|
|
--accent-ink: #14161a;
|
|
--selected: #2c3340;
|
|
--error: #e2827c;
|
|
--shadow: 0 10px 30px -10px rgb(0 0 0 / 70%);
|
|
}
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
font: 14px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
/* The one keyboard surface the page has is `#grid`, and until it is focused
|
|
nothing responds to a key. A visible ring is the affordance that says so. */
|
|
:focus-visible {
|
|
outline: 2px solid var(--accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* ── The app bar ─────────────────────────────────────────────────────────
|
|
One line, and the eye's order across it is: what this is, which document,
|
|
which view, which mode, what keys, what the peer is doing.
|
|
|
|
RESERVED, NOT FITTED (improvise-n6t8). Every child that changes with the
|
|
user's own keystrokes has an allocated width, so a mode change moves
|
|
nothing after it. #hint is the single elastic child and absorbs whatever is
|
|
left, which is why nothing is positioned by it. Why a floor rather than a
|
|
cap on the mode pill, and why the guard is a spec and not the number:
|
|
context/code/clients.md. */
|
|
|
|
header {
|
|
display: flex;
|
|
/* The reserve is a FLOOR, so below the width that holds it the bar must
|
|
reflow rather than widen the document (improvise-tg27). clients.md. */
|
|
flex-wrap: wrap;
|
|
gap: 0.75rem;
|
|
align-items: center;
|
|
flex: none;
|
|
padding: 0.45rem 0.9rem;
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
|
|
header h1 {
|
|
margin: 0;
|
|
font-size: 0.95rem;
|
|
font-weight: 700;
|
|
letter-spacing: -0.02em;
|
|
color: var(--accent);
|
|
flex: none;
|
|
}
|
|
|
|
/* Which build this is (improvise-oujb). Quietest thing on the bar -- it is
|
|
reference, never state -- but it must stay legible enough to quote. */
|
|
#build-version {
|
|
flex: none;
|
|
/* A tag fits whole; a 40-char commit SHA truncates to a prefix long enough
|
|
to name a build, and selecting the text still copies the whole thing. */
|
|
width: 14ch;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: var(--dim);
|
|
font-size: 0.72rem;
|
|
}
|
|
|
|
/* Wide enough for the shipped document's whole title, `name (stored_as)`, so
|
|
the ellipsis is for an unusually long one rather than for the demo. A fixed
|
|
width and not a cap: the dirty marker `[+]` appears and disappears on an
|
|
edit, and a cap would let it shift the bar. */
|
|
#doc-name {
|
|
flex: none;
|
|
width: 34ch;
|
|
/* A reserve wider than the window is not a reserve, it is an overflow. */
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: var(--dim);
|
|
}
|
|
|
|
/* The view's name reads as a label on the grid below it, so it takes the
|
|
chip shape the tile bar uses rather than the mode's filled one. */
|
|
#view-name {
|
|
flex: none;
|
|
width: 16ch;
|
|
/* Centred for the same reason the mode pill is: the reserve is wider than
|
|
most names, and left-aligned text in it reads as a mistake. */
|
|
text-align: center;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
padding: 0.05rem 0.5rem;
|
|
border: 1px solid var(--line-strong);
|
|
border-radius: 999px;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
/* `visibility`, not `display`: the chip is blank before the first frame and
|
|
must still hold its place, or the bar jumps once on load. */
|
|
#view-name:empty {
|
|
visibility: hidden;
|
|
}
|
|
|
|
/* The mode is the single most consequential thing on the page — every key
|
|
means something different in each — so it is the one filled badge. */
|
|
#mode {
|
|
flex: none;
|
|
/* A FLOOR and deliberately no cap, sized for sixteen characters where the
|
|
longest `mode_label` today is fourteen. A cap would absorb an over-long
|
|
label silently; a floor lets it grow, which is what the header-stability
|
|
spec measures. context/code/clients.md. */
|
|
min-width: 21ch;
|
|
text-align: center;
|
|
padding: 0.1rem 0.55rem;
|
|
border-radius: var(--radius);
|
|
background: var(--accent);
|
|
color: var(--accent-ink);
|
|
font-size: 0.78rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.06em;
|
|
}
|
|
|
|
#mode:empty {
|
|
visibility: hidden;
|
|
}
|
|
|
|
/* The hint is reference, not state: one line, ellipsised exactly as the TUI
|
|
truncates it to the terminal's width. It must never push the bar taller. */
|
|
#hint {
|
|
/* Basis ZERO, not `auto`: the hint text is the longest thing on the bar and
|
|
a content basis makes it compete with #status for the shrink, so a longer
|
|
hint narrowed #status until the transport's line WRAPPED and the bar grew
|
|
a whole row taller. From zero it absorbs the remainder and contributes
|
|
nothing to overflow. context/code/clients.md. */
|
|
flex: 1 1 0;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: var(--dim);
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
/* The TRANSPORT's fact alone since improvise-n6t8 — what the peer is doing,
|
|
which is not transient and never auto-hides. The document's own messages
|
|
are the toast's. It is the last child and right-anchored, so it is the one
|
|
thing the reserve above does not cover: it changes only when the peer's
|
|
state changes, and #hint absorbs the difference. */
|
|
#status {
|
|
/* Never shrinks and never wraps: a second line here is a taller bar, which
|
|
is the one thing this whole section exists to prevent. The cap is the
|
|
backstop for an unusually long peer address — app.js puts the whole text
|
|
on `title`, so an ellipsis loses nothing. */
|
|
flex: 0 0 auto;
|
|
/* 52ch is the backstop for a long peer address; `100%` is the one that
|
|
matters on a phone, where 52ch is wider than the window and the bar
|
|
would overflow its own line rather than wrap (improvise-tg27). */
|
|
max-width: min(52ch, 100%);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
margin-left: auto;
|
|
text-align: right;
|
|
color: var(--dim);
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
#status:not(:empty)::before {
|
|
content: "●";
|
|
margin-right: 0.35rem;
|
|
color: var(--accent);
|
|
font-size: 0.7em;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
/* ── The deployment notice ───────────────────────────────────────────────
|
|
Collapsed by default: the one sentence a first visitor needs is the
|
|
summary, and the rest is a configuration reference. clients.md. */
|
|
|
|
#backend-notice {
|
|
flex: none;
|
|
border-bottom: 1px solid var(--line);
|
|
background: var(--accent-soft);
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
#backend-notice > summary {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 0.6rem;
|
|
padding: 0.4rem 0.9rem;
|
|
cursor: pointer;
|
|
list-style: none;
|
|
}
|
|
|
|
#backend-notice > summary::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
|
|
.notice-lede {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* The disclosure's own affordance. It is the ONE place on this page where a
|
|
glyph is chosen by CSS state rather than by a fact from Rust — the open
|
|
state is the browser's, and nothing else knows it. */
|
|
.notice-toggle::after {
|
|
content: "▸ options";
|
|
color: var(--accent);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
#backend-notice[open] .notice-toggle::after {
|
|
content: "▾ hide";
|
|
}
|
|
|
|
.notice-body {
|
|
/* Expanded, this must not take the whole window: `main` is free to shrink
|
|
to nothing, so a short viewport would lose the grid entirely. */
|
|
max-height: 40vh;
|
|
overflow: auto;
|
|
padding: 0 0.9rem 0.6rem;
|
|
max-width: 100ch;
|
|
color: var(--dim);
|
|
}
|
|
|
|
.notice-body p {
|
|
margin: 0 0 0.5rem;
|
|
}
|
|
|
|
#backend-notice code {
|
|
padding: 0 0.25rem;
|
|
border-radius: 3px;
|
|
background: color-mix(in srgb, var(--line-strong) 40%, transparent);
|
|
color: var(--fg);
|
|
}
|
|
|
|
/* ── The tile bar and the page bar ───────────────────────────────────────
|
|
Two strips of chips. Which category is on which axis, and whether an axis
|
|
is navigable, are both decided in Rust; only the weight is here. */
|
|
|
|
#tiles,
|
|
#pages {
|
|
display: flex;
|
|
flex: none;
|
|
gap: 0.35rem;
|
|
align-items: center;
|
|
padding: 0.3rem 0.9rem;
|
|
border-bottom: 1px solid var(--line);
|
|
overflow-x: auto;
|
|
white-space: nowrap;
|
|
scrollbar-width: thin;
|
|
}
|
|
|
|
#tiles .tile,
|
|
#pages .page-axis {
|
|
padding: 0.1rem 0.55rem;
|
|
border: 1px solid var(--line-strong);
|
|
border-radius: 999px;
|
|
background: var(--raised);
|
|
font-size: 0.78rem;
|
|
color: var(--dim);
|
|
}
|
|
|
|
#pages {
|
|
background: var(--surface);
|
|
font-size: 0.85em;
|
|
}
|
|
|
|
/* A Page axis can be moved with `[`/`]` and picked from a menu; a Filter one
|
|
constrains without being navigable (gotcha 27), so only one of them lights
|
|
up and only one of them is a button at all. */
|
|
#pages .navigable {
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
display: inline-flex;
|
|
gap: 0.3rem;
|
|
align-items: center;
|
|
font: inherit;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
#pages .navigable:hover {
|
|
background: var(--accent-soft);
|
|
}
|
|
|
|
/* The arrow says a menu is there. It follows `navigable`, never "is this on
|
|
the page bar" — an arrow over nothing to pick is worse than none. */
|
|
#pages .page-arrow {
|
|
font-size: 1.15em;
|
|
line-height: 1;
|
|
margin-left: -0.05rem;
|
|
}
|
|
|
|
/* The menu itself. Absolutely positioned from the chip it was opened on, and
|
|
outside #pages so a repaint cannot rebuild it under the pointer. */
|
|
#page-menu[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
#page-menu {
|
|
position: fixed;
|
|
z-index: 20;
|
|
min-width: 9rem;
|
|
max-height: 60vh;
|
|
overflow-y: auto;
|
|
padding: 0.2rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--raised);
|
|
border: 1px solid var(--line-strong);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
scrollbar-width: thin;
|
|
}
|
|
|
|
#page-menu .page-item {
|
|
text-align: left;
|
|
padding: 0.2rem 0.5rem;
|
|
border: 0;
|
|
border-radius: 3px;
|
|
background: none;
|
|
color: var(--fg);
|
|
font: inherit;
|
|
font-size: 0.82rem;
|
|
white-space: pre;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* `selected` is what the document is fixed to; `active` is where the keyboard
|
|
is. They start together and part on the first arrow key. */
|
|
#page-menu .page-item.selected {
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
}
|
|
|
|
#page-menu .page-item.active,
|
|
#page-menu .page-item:hover {
|
|
background: var(--selected);
|
|
}
|
|
|
|
/* ── The frame: panels and grid ──────────────────────────────────────── */
|
|
|
|
/* `min-width` as well as `min-height`: without it a flex child's automatic
|
|
minimum is its content, and `main` would be sized by the widest panel plus
|
|
the grid rather than by the window (improvise-tg27). */
|
|
main {
|
|
flex: 1;
|
|
display: flex;
|
|
min-height: 0;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* The panels are fixed-width beside the grid, and a fixed width is a fraction
|
|
of a narrow window: capped, the grid keeps the larger share. clients.md. */
|
|
#categories {
|
|
width: 15rem;
|
|
max-width: 40vw;
|
|
flex: none;
|
|
overflow: auto;
|
|
padding: 0.5rem 0.25rem;
|
|
background: var(--surface);
|
|
border-right: 1px solid var(--line);
|
|
}
|
|
|
|
#formulas,
|
|
#views {
|
|
width: 18rem;
|
|
max-width: 40vw;
|
|
flex: none;
|
|
overflow: auto;
|
|
padding: 0.5rem 0.25rem;
|
|
background: var(--surface);
|
|
border-right: 1px solid var(--line);
|
|
}
|
|
|
|
#categories ul,
|
|
#formulas ul,
|
|
#views ul {
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
#categories li,
|
|
#formulas li,
|
|
#views li {
|
|
padding: 0.12rem 0.5rem;
|
|
border-left: 2px solid transparent;
|
|
border-radius: 0 var(--radius) var(--radius) 0;
|
|
}
|
|
|
|
#views .axes {
|
|
margin-left: 0.5rem;
|
|
color: var(--dim);
|
|
font-size: 0.85em;
|
|
}
|
|
|
|
#views .active .name {
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
}
|
|
|
|
#categories .item {
|
|
padding-left: 1.75rem;
|
|
color: var(--dim);
|
|
}
|
|
|
|
#categories .category {
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* One selection treatment everywhere it can occur: a filled row with an
|
|
accent edge. The tile bar takes the ring instead, being a chip. */
|
|
#categories .selected,
|
|
#formulas .selected,
|
|
#views .selected {
|
|
background: var(--selected);
|
|
border-left-color: var(--accent);
|
|
color: var(--fg);
|
|
}
|
|
|
|
#tiles .selected {
|
|
border-color: var(--accent);
|
|
background: var(--accent);
|
|
color: var(--accent-ink);
|
|
font-weight: 700;
|
|
}
|
|
|
|
#grid {
|
|
flex: 1;
|
|
overflow: auto;
|
|
padding: 0.75rem;
|
|
scrollbar-width: thin;
|
|
}
|
|
|
|
/* ── The table ───────────────────────────────────────────────────────── */
|
|
|
|
table {
|
|
border-collapse: collapse;
|
|
background: var(--raised);
|
|
box-shadow: 0 1px 0 var(--line);
|
|
/* Every number in this grid is a measure, and a pivot table is read down
|
|
its columns: proportional digits make that harder for no gain. */
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
th,
|
|
td {
|
|
border: 1px solid var(--line);
|
|
padding: 0.28rem 0.6rem;
|
|
min-width: 6rem;
|
|
height: 1.75rem;
|
|
text-align: left;
|
|
font-weight: normal;
|
|
}
|
|
|
|
thead th {
|
|
background: var(--surface);
|
|
color: var(--fg);
|
|
font-weight: 700;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
tbody th[scope="row"] {
|
|
background: var(--surface);
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* A group header, on either axis. The TUI draws these green and bold; the
|
|
glyph is already in the text (`indicator` in shell.js). */
|
|
th.group {
|
|
color: var(--accent);
|
|
background: var(--accent-soft);
|
|
font-weight: 700;
|
|
}
|
|
|
|
td.numeric {
|
|
text-align: right;
|
|
}
|
|
|
|
/* Everything else on the grid is an aggregate, and dimming it is what makes
|
|
the settable cells read as the surface you can act on. */
|
|
td:not(.editable) {
|
|
color: var(--dim);
|
|
}
|
|
|
|
/* …and the semantic colours come AFTER it, because they are now the same
|
|
property at the same specificity and only the order separates them. */
|
|
td.error {
|
|
color: var(--error);
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* The only affordance that matters: a cell the server said is settable. */
|
|
td.editable {
|
|
cursor: text;
|
|
}
|
|
|
|
td.editable:hover {
|
|
background: color-mix(in srgb, var(--accent) 8%, transparent);
|
|
}
|
|
|
|
/* The selected cell, and the row header that names it. `:has()` is what lets
|
|
the header light up without the page deriving which row is selected — the
|
|
selection is already on the cell, decided in Rust. */
|
|
td.selected {
|
|
background: var(--accent-soft);
|
|
outline: 2px solid var(--accent);
|
|
outline-offset: -2px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
tbody tr:has(td.selected) th[scope="row"] {
|
|
color: var(--accent);
|
|
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
|
|
}
|
|
|
|
/* A keyboard edit in progress. Left-aligned even in a numeric column,
|
|
because what is drawn is the text being typed rather than a value. */
|
|
td.typing {
|
|
text-align: left;
|
|
outline: 2px solid var(--accent);
|
|
outline-offset: -2px;
|
|
background: var(--accent-soft);
|
|
font-weight: 700;
|
|
}
|
|
|
|
td input {
|
|
width: 100%;
|
|
border: none;
|
|
padding: 0;
|
|
font: inherit;
|
|
color: inherit;
|
|
background: transparent;
|
|
}
|
|
|
|
/* The totals strip. Never summed here — it is projected, because the sum runs
|
|
over rows this viewport scrolled past. */
|
|
tfoot th,
|
|
td.total {
|
|
font-weight: 700;
|
|
border-top: 2px solid var(--line-strong);
|
|
background: var(--surface);
|
|
/* `td:not(.editable)` dims every unsettable cell; a total is unsettable and
|
|
is not incidental, so it opts back out. */
|
|
color: var(--fg);
|
|
}
|
|
|
|
/* ── The minibuffer ──────────────────────────────────────────────────────
|
|
The TUI's bottom line is a terminal convention and a web page's bottom-left
|
|
corner is where nothing is read. The CONTENT is unchanged — it is
|
|
`state.view.mode.minibuffer()`, the same value the TUI draws — so this is
|
|
position, weight and contrast and nothing else. */
|
|
|
|
/* ── The bottom dock ─────────────────────────────────────────────────────
|
|
Out of flow entirely, so nothing in it can push the header or the grid
|
|
(improvise-n6t8). Anchored at the BOTTOM and stacking upward, which is what
|
|
keeps the minibuffer where it was when a toast appears above it. */
|
|
|
|
#bottom-dock {
|
|
position: fixed;
|
|
left: 50%;
|
|
bottom: 1.75rem;
|
|
transform: translateX(-50%);
|
|
z-index: 20;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
max-width: calc(100vw - 2rem);
|
|
/* It floats OVER the grid, so it must not eat a click meant for a cell. */
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* The document's own messages, out of flow and on a timer. `hidden` and not
|
|
an empty node: the text stays as the record of what was last said. */
|
|
#toast {
|
|
max-width: min(46rem, calc(100vw - 2rem));
|
|
padding: 0.45rem 0.8rem;
|
|
border: 1px solid var(--line-strong);
|
|
border-radius: var(--radius);
|
|
background: var(--raised);
|
|
box-shadow: var(--shadow);
|
|
color: var(--fg);
|
|
font-size: 0.85rem;
|
|
overflow-wrap: anywhere;
|
|
animation: toast-in 140ms ease-out;
|
|
}
|
|
|
|
@keyframes toast-in {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(0.35rem);
|
|
}
|
|
}
|
|
|
|
#minibuffer {
|
|
max-width: min(46rem, calc(100vw - 2rem));
|
|
min-width: min(28rem, calc(100vw - 2rem));
|
|
padding: 0.6rem 0.9rem;
|
|
border: 1px solid var(--accent);
|
|
border-radius: var(--radius);
|
|
background: var(--raised);
|
|
box-shadow: var(--shadow);
|
|
font-family: inherit;
|
|
font-size: 1rem;
|
|
color: var(--fg);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
/* The caret. Purely an affordance — it says "this is where typing lands" —
|
|
and it is drawn by CSS rather than written into the text, because the text
|
|
is the host's and the page adds nothing to it. */
|
|
#minibuffer::after {
|
|
content: "▏";
|
|
color: var(--accent);
|
|
animation: minibuffer-caret 1.1s steps(1) infinite;
|
|
}
|
|
|
|
@keyframes minibuffer-caret {
|
|
50% {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
/* `MinibufColor`, resolved to a terminal-descended colour exactly as
|
|
`draw.rs::minibuf_fg` resolves it for the TUI. This is the last
|
|
translation and the only thing about the prompt that is the page's. */
|
|
#minibuffer.green {
|
|
border-color: #6aa84f;
|
|
color: #4f7f3c;
|
|
}
|
|
#minibuffer.cyan {
|
|
border-color: #3f9ba3;
|
|
color: #2f7980;
|
|
}
|
|
#minibuffer.yellow {
|
|
border-color: #b8952f;
|
|
color: #8a6f21;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
#minibuffer.green {
|
|
color: #9edb8c;
|
|
}
|
|
#minibuffer.cyan {
|
|
color: #7fd8e0;
|
|
}
|
|
#minibuffer.yellow {
|
|
color: #e0cf7f;
|
|
}
|
|
}
|
|
|
|
/* ── The help overlay ────────────────────────────────────────────────────
|
|
Content is improvise_protocol::help's; this is paint. */
|
|
|
|
#help {
|
|
position: fixed;
|
|
inset: 2rem;
|
|
z-index: 30;
|
|
overflow: auto;
|
|
padding: 1.25rem 1.5rem;
|
|
border: 1px solid var(--accent);
|
|
border-radius: var(--radius);
|
|
background: var(--bg);
|
|
box-shadow: var(--shadow);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.help-tabs {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
border-bottom: 1px solid var(--line);
|
|
padding-bottom: 0.5rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.help-tab {
|
|
color: var(--dim);
|
|
}
|
|
|
|
.help-tab.active {
|
|
color: var(--accent);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.help-body {
|
|
display: grid;
|
|
grid-template-columns: 16rem 1fr;
|
|
gap: 0 1rem;
|
|
margin: 0;
|
|
}
|
|
|
|
.help-body dt,
|
|
.help-body dd {
|
|
margin: 0;
|
|
min-height: 1.2em;
|
|
}
|
|
|
|
/* Only the key column is accented: the other kinds are prose rows that
|
|
happen to be `dt`s because they span both columns. */
|
|
.help-keys {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.help-heading {
|
|
grid-column: 1 / -1;
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.help-banner {
|
|
grid-column: 1 / -1;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.help-text,
|
|
.help-accent,
|
|
.help-blank {
|
|
grid-column: 1 / -1;
|
|
white-space: pre;
|
|
}
|
|
|
|
.help-dim {
|
|
color: var(--dim);
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
* {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
}
|
|
}
|