/* 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 `[`/`]` 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 ──────────────────────────────────────── */ 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; } }