Files
improvise/web/style.css
T
Edward Langley 926396352f fix: seat the build stamp in #120's redesigned bar, and stop it faking a ticket id
Three things the merge with #120 (ui/shell-polish) actually required, none of
which a clean auto-merge could have told us.

MOVED, not preserved. #120 left <header>'s markup alone but gave every child
explicit styling and a stated reading order -- "what this is, which document,
which view, which mode, what keys, what the peer is doing". A build identifier
is "what this is", so the span moves from after #status to beside <h1>. Keeping
it at the tail was loyalty to my own first draft: it read as metadata trailing
#status, and with #hint's `flex: 1 1 auto` fighting for the slack a 40-char
commit SHA there is the thing that crowds the bar.

STYLED, because unstyled is illegible-by-default in a design system. It takes
`var(--dim)` -- the same token #hint and #status use, so it tracks both themes
by construction rather than by a hand-picked colour -- at 0.72rem, and clamps
to 14ch. Measured under Chromium on the merged tree: a tag renders whole
(scrollWidth == clientWidth, 69px), a 40-char SHA truncates to a 13-character
prefix with an ellipsis, and `header` does not overflow in either case
(scrollWidth == clientWidth == 1280). Contrast of rgb(107,103,95) on
rgb(244,241,234) is ~5.0:1, above AA for small text. Selecting the text still
copies the whole SHA -- text-overflow is a rendering clamp, not a truncation.

NOT AN ID. `improvise-web.tar.gz` puts the token `improvise-web` in the tree,
and AGENTS.md's audit matches `improvise-[a-z0-9]{3,8}` -- so the artifact name
resolved to no ticket and read exactly like a dead id, which AGENTS.md calls
worse than one. `improvise-installer.sh` escapes only by being ten characters,
which is luck rather than a rule, so the exclusion list gains `web` and the
paragraph gains the general case.

RELEASING.md (which arrived in this same merge, from improvise-gx5d) listed
what a tag publishes and was made wrong by omission; it now names the web
capture and says plainly that it is not what Pages is serving.

Verified on the merged tree: 1631 cargo tests over 37 binaries, node --test
47 -> 50 (so #120's three new shell tests survived the merge rather than being
silently dropped), 42 Playwright tests green under Chromium, and the stamp
gate re-proved by injecting the missed substitution again -- a header rewrite
does not carry the old fault injection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 23:01:08 -07:00

631 lines
15 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. */
header {
display: flex;
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. */
max-width: 14ch;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--dim);
font-size: 0.72rem;
}
#doc-name {
flex: none;
/* 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. */
max-width: 36ch;
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;
padding: 0.05rem 0.5rem;
border: 1px solid var(--line-strong);
border-radius: 999px;
font-size: 0.8rem;
}
#view-name:empty {
display: none;
}
/* 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;
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 {
display: none;
}
/* 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 {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--dim);
font-size: 0.8rem;
}
/* Two facts composed in one string (`shell.js::composeStatus`), so the dot
marks the whole line rather than either half. */
#status {
flex: 0 1 auto;
min-width: 0;
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 `[`/`]`; a Filter one constrains without
being navigable (gotcha 27), which is why only one of them lights up. */
#pages .navigable {
border-color: var(--accent);
color: var(--accent);
font-weight: 600;
}
/* ── The frame: panels and grid ──────────────────────────────────────── */
main {
flex: 1;
display: flex;
min-height: 0;
}
#categories {
width: 15rem;
flex: none;
overflow: auto;
padding: 0.5rem 0.25rem;
background: var(--surface);
border-right: 1px solid var(--line);
}
#formulas,
#views {
width: 18rem;
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. */
#minibuffer {
position: fixed;
left: 50%;
bottom: 1.75rem;
transform: translateX(-50%);
z-index: 20;
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);
/* It floats OVER the grid, so it must not eat a click meant for a cell. */
pointer-events: none;
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;
}
}