/* ============================================================ Wellformed — spreadsheet UX over a relational engine. A cool "database console" instrument. IBM Plex Sans/Mono. Signature: the live SQL bar — every grid is a query. ============================================================ */ :root { --paper: #f4f7fa; --panel: #ffffff; --ink: #17273b; --ink-soft: #5a6b80; --line: #dce4ec; --line-soft: #e9eff5; --band: #eaf0f6; --cobalt: #2050d8; --cobalt-soft: #e7edfc; --cobalt-deep: #1a44bc; --rel: #0b7a55; --rel-soft: #e2f3ec; --rel-line: #bfe3d3; --amber: #a16207; --amber-soft: #fbf1dc; --danger: #b3261e; --danger-soft: #fbeae9; --radius: 5px; --shadow-pop: 0 10px 30px rgba(23, 39, 59, 0.18); --shadow-modal: 0 18px 50px rgba(23, 39, 59, 0.3); } * { box-sizing: border-box; } html, body, #root { height: 100%; margin: 0; } body { background: var(--paper); color: var(--ink); font-family: 'IBM Plex Sans', system-ui, -apple-system, sans-serif; font-size: 13px; -webkit-font-smoothing: antialiased; } ::selection { background: var(--cobalt-soft); } button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; padding: 0; } button:disabled { cursor: not-allowed; opacity: 0.5; } input, select, textarea { font: inherit; color: inherit; } :focus-visible { outline: 2px solid var(--cobalt); outline-offset: 1px; } .mono { font-family: 'IBM Plex Mono', ui-monospace, monospace; } .visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } /* ---- shell ---- */ .app { height: 100%; display: flex; flex-direction: column; background: var(--paper); } .top { display: flex; align-items: center; gap: 12px; padding: 8px 14px; background: var(--panel); border-bottom: 1px solid var(--line); flex: none; } .wordmark { font-family: 'Bricolage Grotesque', 'IBM Plex Sans', sans-serif; font-weight: 800; font-size: 17px; letter-spacing: -0.02em; white-space: nowrap; } .wordmark span { color: var(--cobalt); } .tag { font-family: 'IBM Plex Mono', monospace; font-size: 10px; color: var(--ink-soft); border: 1px solid var(--line); border-radius: 3px; padding: 2px 6px; letter-spacing: 0.04em; white-space: nowrap; } .grow { flex: 1; } .iconbtn { display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); font-weight: 500; color: var(--ink); white-space: nowrap; } .iconbtn:hover:not(:disabled) { border-color: var(--cobalt); color: var(--cobalt); } .iconbtn.active { background: var(--amber-soft); border-color: var(--amber); color: var(--amber); } .iconbtn.danger:hover:not(:disabled) { border-color: var(--danger); color: var(--danger); } /* Stands in for a control's word where the word will not fit; see the narrow layout at the foot of this file. */ .initial { display: none; } .searchbox { display: flex; align-items: center; gap: 6px; border: 1px solid var(--line); border-radius: var(--radius); padding: 0 8px; background: var(--panel); min-width: 190px; } .searchbox input { border: none; background: none; padding: 5px 0; width: 100%; font-size: 12.5px; } .searchbox input:focus { outline: none; } .searchbox svg { color: var(--ink-soft); flex: none; } .body { flex: 1; display: flex; min-height: 0; } /* Both of these only come into play at the narrow breakpoint below. */ .sidebar-toggle { display: none; } .backdrop { display: none; position: fixed; inset: 0; z-index: 35; background: rgba(23, 39, 59, 0.35); } @media (prefers-reduced-motion: reduce) { .side { transition: none; } } /* ---- sidebar ---- */ .side { width: 236px; flex: none; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; overflow-y: auto; padding: 0 8px 16px; scrollbar-width: thin; scrollbar-color: var(--line) transparent; } .side h3 { font-size: 10px; font-weight: 600; letter-spacing: 0.1em; color: var(--ink-soft); text-transform: uppercase; margin: 16px 8px 6px; display: flex; align-items: center; gap: 6px; } .side h3 button { margin-left: auto; color: var(--ink-soft); font-size: 14px; line-height: 1; } .side h3 button:hover { color: var(--cobalt); } /* A shelf over a run of tables. The h3 above is an eyebrow — small caps, whispered — so the shelf, oddly, gets to be the louder one: it is a real heading you read and click, not a section marker. */ .side h4.tblgroup { margin: 8px 0 0; } .side h4.tblgroup button { width: 100%; display: flex; align-items: center; gap: 7px; padding: 6px 8px; font-size: 13px; font-weight: 650; color: var(--ink); text-align: left; border-radius: var(--radius); } .side h4.tblgroup button:hover { background: var(--band); } /* Muted and small on purpose: the caret answers a question you only ask while folding, and must not compete with the heading it belongs to. */ .side h4.tblgroup .caret { flex: none; width: 14px; margin-left: auto; text-align: center; font-size: 11px; line-height: 1; color: var(--ink-soft); } /* Reordering by drag: the same language the column headers speak. */ .tbl-item.dragging, .side h4.tblgroup button.dragging { opacity: 0.4; } /* The line sits on the edge the drop will land on: top half of the target means before it, lower half means after it. It marks the whole block — a table travels with its views, so "after" means below them. */ .tblblock.drop-before, .side h4.tblgroup button.drop-before { box-shadow: inset 0 2px 0 var(--cobalt); } .tblblock.drop-after, .side h4.tblgroup button.drop-after { box-shadow: inset 0 -2px 0 var(--cobalt); } .tbl-item { width: 100%; text-align: left; padding: 6px 8px; display: flex; align-items: center; gap: 8px; font-weight: 500; border-radius: var(--radius); } /* Under a heading a table indents; a table with no shelf keeps the edge. */ .shelf .tbl-item { padding-left: 20px; } .tbl-item:hover { background: var(--band); } .tbl-item.on { background: var(--cobalt-soft); color: var(--cobalt); font-weight: 600; } .tbl-item .n, .view-item .n { margin-left: auto; font-family: 'IBM Plex Mono', monospace; font-size: 10px; color: var(--ink-soft); } .tbl-item.on .n, .view-item.on .n { color: inherit; opacity: 0.75; } /* The views hang off their table on a guide line, the way a tree draws children: the line starts under the table's first letter. */ .viewlist { margin: 1px 0 5px 11px; border-left: 1px solid var(--line); } .shelf .viewlist { margin-left: 23px; } .view-item { width: 100%; text-align: left; padding: 4px 8px 4px 10px; font-size: 12px; color: var(--ink-soft); display: flex; align-items: center; gap: 8px; border-radius: 0 var(--radius) var(--radius) 0; } .view-item:hover { background: var(--band); color: var(--ink); } .view-item.on { background: var(--cobalt-soft); color: var(--cobalt); font-weight: 600; } .schema { margin: 4px 8px 12px; border-left: 1px solid var(--line); padding-left: 10px; } .rule-row { font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; color: var(--ink-soft); padding: 3px 0; } .rule-row b { color: var(--ink); font-weight: 500; } /* The rule fades when it is off; the button that turns it back on does not, because a faded control is exactly what you cannot find in that state. */ .rule-row.off > div { opacity: 0.45; text-decoration: line-through; } /* Set apart from the rule it acts on: the same monospace at the same size, unadorned, read as another line of the rule rather than as a control. */ .ruletoggle { margin: 4px 0 2px 10px; padding: 2px 8px; border: 1px solid var(--line); border-radius: 3px; background: var(--panel); font-family: inherit; font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; text-decoration: none; color: var(--ink-soft); } .ruletoggle:hover { border-color: var(--cobalt); color: var(--cobalt); } /* ---- main column ---- */ .main { flex: 1; display: flex; flex-direction: column; min-width: 0; } .viewbar { display: flex; align-items: center; gap: 6px; padding: 7px 12px; border-bottom: 1px solid var(--line); background: var(--panel); flex-wrap: wrap; flex: none; } .viewname { font-weight: 600; color: var(--ink); white-space: nowrap; padding-right: 4px; } .filterchip { display: inline-flex; align-items: center; gap: 5px; background: var(--band); border: 1px solid var(--line); border-radius: var(--radius); padding: 3px 8px; font-family: 'IBM Plex Mono', monospace; font-size: 11px; white-space: nowrap; } .filterchip.saved { background: var(--cobalt-soft); border-color: #c9d7f7; color: var(--cobalt); } .filterchip button { color: var(--ink-soft); font-size: 13px; line-height: 1; } .filterchip button:hover { color: var(--danger); } .viewbar select, .viewbar input { border: 1px solid var(--line); border-radius: var(--radius); padding: 3px 6px; background: var(--panel); font-size: 12px; } .viewbar input { width: 110px; } /* ---- formula bar ---- */ .fbar { display: flex; align-items: center; border-bottom: 1px solid var(--line); background: var(--panel); flex: none; } /* Empty, it is only holding its place; it should not ask to be read. */ .fbar.quiet .addr, .fbar.quiet .mark { opacity: 0.45; } .fbar .addr { font-family: 'IBM Plex Mono', monospace; font-size: 11.5px; padding: 7px 12px; min-width: 190px; color: var(--ink-soft); border-right: 1px solid var(--line); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Fixed width: the marker is a different length per column type, and the value field must not slide sideways as the selection moves across a row. */ .fbar .mark { font-family: 'IBM Plex Mono', monospace; color: var(--cobalt); padding: 0 10px; min-width: 54px; text-align: center; font-weight: 600; font-size: 12px; flex: none; } /* A value too long for the strip scrolls rather than being clipped: reading the whole of a long cell is half of what this row is still for. */ .fbar .value { flex: 1; min-width: 0; overflow-x: auto; white-space: pre; font-family: 'IBM Plex Mono', monospace; font-size: 12px; padding: 7px 8px 7px 0; scrollbar-width: thin; } .fbar .hint { font-family: 'IBM Plex Mono', monospace; font-size: 10px; color: var(--ink-soft); padding-right: 12px; white-space: nowrap; } /* ---- grid ---- */ /* The grid and, above it, the row-adding bar. The bar lives outside the scroller so that it is simply always there — at the end of the table it sat under every loaded row, and on a long table reaching it meant scrolling through all of them. */ .gridarea { flex: 1; display: flex; flex-direction: column; min-height: 0; } .gridwrap { flex: 1; overflow: auto; background: var(--panel); position: relative; outline: none; } table.grid { border-collapse: separate; border-spacing: 0; font-size: 12.5px; table-layout: fixed; } .grid th { position: sticky; top: 0; z-index: 2; background: var(--band); border-bottom: 1px solid var(--line); border-right: 1px solid var(--line-soft); padding: 0; text-align: left; overflow: hidden; } .grid th .hbtn { display: flex; align-items: center; gap: 5px; width: 100%; padding: 7px 10px; font-weight: 600; font-size: 11.5px; color: var(--ink); overflow: hidden; } .grid th .hbtn > .label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .grid th .hbtn:hover { color: var(--cobalt); } .grid th.dragging { opacity: 0.4; } .grid th.drop-target { box-shadow: inset 2px 0 0 var(--cobalt); } .grid th .ty { font-family: 'IBM Plex Mono', monospace; font-size: 9px; color: var(--ink-soft); font-weight: 400; letter-spacing: 0.05em; flex: none; } .grid th .sortmark { color: var(--cobalt); flex: none; margin-left: auto; } .grid th .broken { color: var(--danger); flex: none; } .resizer { position: absolute; top: 0; right: 0; width: 7px; height: 100%; cursor: col-resize; user-select: none; touch-action: none; } .resizer:hover, .resizer.active { background: var(--cobalt); opacity: 0.5; } .grid th.rownum, .grid td.rownum { width: 44px; min-width: 44px; text-align: center; font-family: 'IBM Plex Mono', monospace; font-size: 10px; color: var(--ink-soft); background: var(--band); border-right: 1px solid var(--line); position: sticky; left: 0; z-index: 1; } .grid th.rownum { z-index: 4; } /* The first data column labels the row, so it stays put while the rest of the table scrolls sideways — otherwise you lose track of which row you are in. */ .grid th.frozen, .grid td.frozen { position: sticky; left: 44px; /* A lifted edge, so the sliver of the column scrolling beneath reads as underneath rather than as leftover pixels. */ box-shadow: 1px 0 0 var(--line), 3px 0 6px -2px rgba(23, 39, 59, 0.14); } .grid th.frozen { z-index: 3; } .grid td.frozen { z-index: 1; background: var(--panel); } /* The row being composed. Marked as provisional so it never reads as saved. */ .grid tr.draftrow td { background: var(--cobalt-soft); } .grid tr.draftrow td.rownum { color: var(--cobalt); font-weight: 600; } .grid tr.draftrow td.frozen { background: var(--cobalt-soft); } /* The aggregate row. Pinned to the bottom of the scroller the way the header is pinned to the top, and part of the same table so it can never drift out of alignment with the columns it is summarising. */ .grid tfoot td { position: sticky; bottom: 0; z-index: 2; background: var(--band); border-top: 1px solid var(--line); border-right: 1px solid var(--line-soft); padding: 0; overflow: hidden; } .grid tfoot td.rownum { z-index: 4; border-top: 1px solid var(--line); font-size: 12px; color: var(--ink-soft); } .grid tfoot td.frozen { z-index: 3; background: var(--band); } .grid tfoot td.filler { border-right: none; } .sumbtn { display: flex; align-items: baseline; gap: 8px; width: 100%; padding: 5px 10px; font-size: 11.5px; overflow: hidden; } .sumbtn .fn { font-family: 'IBM Plex Mono', monospace; font-size: 9px; letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink-soft); flex: none; } .sumbtn .val { margin-left: auto; font-weight: 600; font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* An unsummarised column offers itself only under the pointer: a row of sigmas across every column would read as data rather than as an invitation. */ .sumbtn .pick { margin-left: auto; color: transparent; } .grid tfoot td:hover .sumbtn .pick { color: var(--ink-soft); } .sumbtn:hover .fn, .sumbtn:hover .val { color: var(--cobalt); } .sumbtn .broken { margin-left: auto; color: var(--danger); font-weight: 700; } .addrow.drafting { display: flex; align-items: center; gap: 10px; color: var(--cobalt); } .addrow.drafting button { color: var(--ink-soft); font-size: 11px; } .addrow.drafting button:hover { color: var(--ink); } /* A link chip is a way through to the other row, so it has to look like one. */ .chip.linked { cursor: pointer; } .chip.linked:hover { border-color: var(--rel); filter: brightness(0.97); } .record { display: flex; flex-direction: column; } .record-row { display: flex; gap: 12px; padding: 6px 0; border-bottom: 1px solid var(--line-soft); font-size: 12.5px; } .record-row:last-child { border-bottom: none; } .record-row .k { flex: none; width: 132px; color: var(--ink-soft); font-family: 'IBM Plex Mono', monospace; font-size: 11px; padding-top: 1px; } .record-row .v { color: var(--ink); word-break: break-word; } /* Absorbs the space left when the columns do not fill the window, so the header band and the row rules run to the edge instead of stopping short. */ .grid th.filler, .grid td.filler { border-right: none; padding: 0; } .grid tr.row-selected td.rownum { background: var(--cobalt-soft); color: var(--cobalt); } .grid td { border-bottom: 1px solid var(--line-soft); border-right: 1px solid var(--line-soft); padding: 0; background: var(--panel); overflow: hidden; } .cell { height: 30px; display: flex; align-items: center; padding: 4px 10px; cursor: cell; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .cell > span { overflow: hidden; text-overflow: ellipsis; } .cell.num { justify-content: flex-end; font-family: 'IBM Plex Mono', monospace; font-size: 12px; } .cell.datev { font-family: 'IBM Plex Mono', monospace; font-size: 11.5px; } .cell.computed { background: #f7faff; color: #3a4c68; cursor: default; } .cell.errored { background: var(--danger-soft); color: var(--danger); font-family: 'IBM Plex Mono', monospace; font-size: 11px; } td.sel .cell { box-shadow: inset 0 0 0 2px var(--cobalt); background: var(--cobalt-soft); } td.sel .cell.computed { background: #dde7fb; } /* The block selection: a cell is in it where a chosen row and a chosen column overlap. Softer than the cursor, which keeps its own ring on top. */ td.insel .cell { background: var(--cobalt-soft); } .grid th.selcol .hbtn, .grid td.rownum.selrow { background: var(--cobalt-soft); color: var(--cobalt); } /* Mid-drag the pointer is choosing cells, not the text inside them. */ .gridwrap.dragsel, .gridwrap.dragsel .cell { user-select: none; cursor: cell; } .chip { display: inline-flex; align-items: center; gap: 5px; background: var(--rel-soft); color: var(--rel); border: 1px solid var(--rel-line); border-radius: 4px; padding: 1px 7px; font-size: 11.5px; font-weight: 500; max-width: 100%; overflow: hidden; } .chip span { overflow: hidden; text-overflow: ellipsis; } .chip svg { flex: none; } .pill { display: inline-block; border-radius: 99px; padding: 1px 9px; font-size: 11px; font-weight: 500; max-width: 100%; overflow: hidden; text-overflow: ellipsis; } .cell input, .cell select { width: 100%; border: none; background: none; font-family: 'IBM Plex Mono', monospace; font-size: 12px; padding: 0; } .cell input:focus, .cell select:focus { outline: none; } .cell input[type='checkbox'] { width: auto; cursor: pointer; } /* ---- link picker ---- */ /* The foreign-key editor: a search field in the cell, the rows the column may point at in a fixed panel under it — fixed because grid cells clip, the same escape the context menus use. */ .linkpick { width: 100%; } .linkpick-panel { position: fixed; z-index: 40; background: var(--panel); border: 1px solid var(--line); border-radius: 6px; box-shadow: var(--shadow-pop); padding: 4px; max-width: 360px; max-height: 260px; overflow-y: auto; } .linkpick-panel .hit { display: block; width: 100%; text-align: left; padding: 6px 10px; border-radius: 4px; font-size: 12.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* One highlight, moved by the arrows and the mouse alike — not a second, different-looking one for :hover, which would read as two cursors. */ .linkpick-panel .hit.active { background: var(--band); } .linkpick-panel .hit.clear { color: var(--ink-soft); } .linkpick-note { padding: 6px 10px; font-size: 11px; color: var(--ink-soft); } .addrow { display: flex; align-items: center; gap: 6px; padding: 8px 12px; color: var(--ink-soft); font-weight: 500; width: 100%; flex: none; background: var(--panel); border-bottom: 1px solid var(--line); } .addrow:hover:not(:disabled) { color: var(--cobalt); background: var(--band); } .gridstate { padding: 28px 16px; color: var(--ink-soft); text-align: center; font-size: 12.5px; } .gridstate.error { color: var(--danger); } /* ---- SQL status bar — the signature ---- */ .sqlbar { border-top: 1px solid var(--line); background: var(--ink); color: #c7d4e6; padding: 6px 14px; font-family: 'IBM Plex Mono', monospace; font-size: 11.5px; flex: none; } .sqlbar-line { display: flex; gap: 12px; align-items: baseline; cursor: pointer; } .sqlbar .disclose { color: #7ea4ff; font-family: inherit; font-size: 9.5px; letter-spacing: 0.12em; text-transform: uppercase; } .sqlbar .disclose:hover { color: #a8c2ff; } /* Opened on request, so it should be readable rather than a one-line ribbon: wrap it and cap the height instead of scrolling sideways. The cap sits on the container, so both statements scroll as one. */ .sqlbar.open .stmts { margin-top: 7px; max-height: 30vh; overflow-y: auto; scrollbar-width: thin; } /* Each statement in its own box: the rows query and the aggregate query are two statements, and without an edge between them they read as one. */ .sqlbar.open .stmt { display: block; padding: 7px 10px; border: 1px solid rgba(255, 255, 255, 0.14); border-radius: var(--radius); background: rgba(255, 255, 255, 0.04); white-space: pre-wrap; word-break: break-word; line-height: 1.55; tab-size: 4; } .sqlbar.open .stmt + .stmt { margin-top: 8px; } .sqlbar .lbl { color: #6e86a6; font-size: 9.5px; letter-spacing: 0.12em; flex: none; } .sqlbar .kw { color: #7ea4ff; font-weight: 600; } .sqlbar .str { color: #8fd8b8; } .sqlbar .num { color: #f0c674; } .sqlbar .fn { color: #c8a2ff; } .sqlbar .id { color: #e8eef7; } .sqlbar .op, .sqlbar .plain { color: #93a7c0; } /* The artificial variables: computed columns folded back into their names, defined once above the statement. Italic, because SQL has no such thing. */ .sqlbar .var { color: #6fd3e0; font-style: italic; } .sqlbar .copy { color: #6e86a6; flex: none; font-size: 10px; letter-spacing: 0.06em; } .sqlbar .copy:hover { color: #c7d4e6; } /* ---- audit panel ---- */ .audit { width: 300px; flex: none; border-left: 1px solid var(--line); background: var(--panel); display: flex; flex-direction: column; min-height: 0; } .audit h2 { font-size: 11px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-soft); padding: 12px 14px; border-bottom: 1px solid var(--line); margin: 0; display: flex; justify-content: space-between; align-items: center; gap: 8px; } .audit h2 button { color: var(--ink-soft); font-size: 15px; line-height: 1; } .audit h2 button:hover { color: var(--ink); } .audit .scope { padding: 7px 14px; border-bottom: 1px solid var(--line-soft); font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; color: var(--ink-soft); display: flex; align-items: center; gap: 6px; } .audit .list { flex: 1; overflow-y: auto; } .ae { padding: 9px 14px; border-bottom: 1px solid var(--line-soft); font-size: 11.5px; } .ae .meta { display: flex; gap: 6px; align-items: center; margin-bottom: 3px; } .ae .who { font-weight: 600; } .ae .t { font-family: 'IBM Plex Mono', monospace; font-size: 9.5px; color: var(--ink-soft); margin-left: auto; flex: none; } .ae .loc { font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; color: var(--ink-soft); word-break: break-word; } .ae .delta { font-family: 'IBM Plex Mono', monospace; font-size: 11px; margin-top: 2px; word-break: break-word; } .ae .old { color: var(--ink-soft); text-decoration: line-through; } .ae .new { color: var(--cobalt); font-weight: 500; } .badge { font-size: 9px; font-weight: 600; letter-spacing: 0.06em; border-radius: 3px; padding: 1px 5px; text-transform: uppercase; flex: none; } .badge.workflow { background: var(--amber-soft); color: var(--amber); } .badge.schema { background: var(--rel-soft); color: var(--rel); } .badge.insert, .badge.view { background: var(--band); color: var(--ink-soft); } .badge.delete { background: var(--danger-soft); color: var(--danger); } .badge.auth { background: var(--cobalt-soft); color: var(--cobalt); } /* An agent's label is a name, not a category, so it keeps its own casing and is allowed to be longer than the one-word kind badges beside it. */ .badge.agent { background: var(--rel-soft); color: var(--rel); text-transform: none; letter-spacing: 0; max-width: 12ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .loadmore { width: 100%; padding: 10px; color: var(--ink-soft); font-weight: 500; } .loadmore:hover { color: var(--cobalt); background: var(--band); } /* ---- menus ---- */ .menu { position: fixed; z-index: 40; background: var(--panel); border: 1px solid var(--line); border-radius: 6px; box-shadow: var(--shadow-pop); padding: 4px; min-width: 190px; } .menu button { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; padding: 6px 10px; border-radius: 4px; font-size: 12.5px; } .menu button:hover:not(:disabled) { background: var(--band); } .menu button.danger:hover:not(:disabled) { background: var(--danger-soft); color: var(--danger); } .menu button .kbd { margin-left: auto; font-family: 'IBM Plex Mono', monospace; font-size: 10px; color: var(--ink-soft); } .menu hr { border: none; border-top: 1px solid var(--line-soft); margin: 4px 0; } /* ---- modal ---- */ .scrim { position: fixed; inset: 0; background: rgba(23, 39, 59, 0.4); display: flex; align-items: center; justify-content: center; z-index: 50; padding: 20px; } .modal { background: var(--panel); border-radius: 8px; width: 420px; max-width: 100%; max-height: 100%; display: flex; flex-direction: column; box-shadow: var(--shadow-modal); } .modal.wide { width: 640px; } .modal h2 { font-size: 14px; margin: 0; padding: 14px 18px; border-bottom: 1px solid var(--line); flex: none; } .modal .mb { padding: 14px 18px; display: flex; flex-direction: column; gap: 10px; overflow-y: auto; } .modal label { font-size: 11px; font-weight: 600; color: var(--ink-soft); display: flex; flex-direction: column; gap: 4px; } .modal label.row { flex-direction: row; align-items: center; gap: 8px; } .modal input, .modal select { border: 1px solid var(--line); border-radius: var(--radius); padding: 6px 8px; font-size: 12.5px; background: var(--panel); } .modal input[type='checkbox'] { width: auto; } .modal .hint { font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; color: var(--ink-soft); font-weight: 400; line-height: 1.5; } .modal .hint.bad { color: var(--danger); } .modal .hint.good { color: var(--rel); } /* ---- the constraint check, inside the column dialog --------------------- */ .tighten { border: 1px solid var(--line); border-left: 3px solid var(--cobalt); border-radius: var(--radius); background: var(--paper); padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; font-size: 11.5px; font-weight: 400; color: var(--ink); } .tighten-head { font-weight: 600; font-size: 12px; } .tighten .good { color: var(--rel); } .tighten .bad { color: var(--danger); } .tighten .muted { color: var(--ink-soft); } .tighten-problem { display: flex; flex-direction: column; gap: 4px; } .tighten .samples { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; } .tighten .sample { font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; background: var(--panel); border: 1px solid var(--line); border-radius: 3px; padding: 1px 5px; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tighten-rewrites { display: flex; flex-wrap: wrap; gap: 10px; font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; color: var(--ink-soft); } .tighten-reading { display: flex; flex-wrap: wrap; gap: 12px; } .tighten-reading label { font-size: 10.5px; } .tighten-offer { display: flex; flex-direction: column; gap: 6px; padding-top: 6px; border-top: 1px solid var(--line); } .tighten-offer .btn.small { align-self: flex-start; padding: 3px 9px; font-size: 11px; } .tighten-sql pre { margin: 6px 0 0; padding: 8px 10px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); font-size: 10.5px; line-height: 1.6; overflow-x: auto; white-space: pre; color: var(--ink-soft); } .tighten .linkish { color: var(--cobalt); font-size: 11px; font-weight: 600; align-self: flex-start; } /* ---- the tidy-up walk, one column per step ------------------------------ */ /* Spellings on the left, the survivor on the right, the arrow between them. */ .tidy-cluster { display: flex; align-items: center; gap: 8px; } .tidy-cluster .row { flex: 1; min-width: 0; } .tidy-to { font-family: 'IBM Plex Mono', monospace; font-size: 11px; flex: 1 1 160px; min-width: 0; max-width: 300px; } /* The scan only offers values short enough to read whole (see normalize.ts), so nothing here may ellipsize them — an offer you cannot read is no offer. */ .tidy .sample { max-width: none; } /* A column's constraints, marked in the header the way SQL marks them. */ .grid th .cons { font-family: 'IBM Plex Mono', monospace; font-size: 10px; color: var(--amber); font-weight: 700; flex: none; } .modal .mf { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 18px; border-top: 1px solid var(--line); flex: none; } .modal .mf .grow { flex: 1; } .btn { padding: 6px 14px; border-radius: var(--radius); font-weight: 600; border: 1px solid var(--line); background: var(--panel); } .btn.primary { background: var(--cobalt); border-color: var(--cobalt); color: #fff; } .btn.primary:hover:not(:disabled) { background: var(--cobalt-deep); } .btn.destructive { background: var(--danger); border-color: var(--danger); color: #fff; } .btn:not(.primary):not(.destructive):hover:not(:disabled) { border-color: var(--ink-soft); } table.plain { width: 100%; border-collapse: collapse; font-size: 12.5px; } table.plain th { text-align: left; font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-soft); padding: 6px 8px; border-bottom: 1px solid var(--line); } table.plain td { padding: 7px 8px; border-bottom: 1px solid var(--line-soft); vertical-align: middle; } table.plain tr.disabled td { opacity: 0.5; } table.plain select { border: 1px solid var(--line); border-radius: 4px; padding: 3px 5px; font-size: 12px; } /* ---- login ---- */ .login { min-height: 100%; display: flex; align-items: center; justify-content: center; padding: 24px; background: var(--paper); } .login-card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; box-shadow: 0 12px 40px rgba(23, 39, 59, 0.1); width: 360px; max-width: 100%; padding: 28px; } .login-card .wordmark { font-size: 26px; margin-bottom: 4px; } .login-card .tagline { color: var(--ink-soft); font-family: 'IBM Plex Mono', monospace; font-size: 11px; margin-bottom: 22px; } .login-card form { display: flex; flex-direction: column; gap: 12px; } .login-card label { font-size: 11px; font-weight: 600; color: var(--ink-soft); display: flex; flex-direction: column; gap: 5px; } .login-card input, .login-card select { border: 1px solid var(--line); border-radius: var(--radius); padding: 8px 10px; font-size: 13px; background: var(--panel); } /* A hint inside a label inherits the label's 600 weight; the explanation should not shout louder than the field it explains. */ .login-card label .hint { font-weight: 400; } .login-card .btn { padding: 9px 14px; margin-top: 4px; } .formerror { background: var(--danger-soft); color: var(--danger); border: 1px solid #f0cfcc; border-radius: var(--radius); padding: 8px 10px; font-size: 12px; } /* Signup and the workspace picker ask for more than a login does — wide enough that the starting points sit two across rather than in one column. */ .login-card.wide { width: 620px; } .login-card .hint a, .login-card .linkbtn { color: var(--accent); } .login-card > .hint { margin: 16px 0 0; font-size: 12px; } .login-card label.check { flex-direction: row; align-items: center; gap: 8px; font-weight: 500; } .login-card label.check input { width: auto; } .linkbtn { background: none; border: 0; padding: 0; font: inherit; color: var(--accent); cursor: pointer; text-decoration: underline; } /* ---- workspaces ---- */ .wslist { list-style: none; margin: 0 0 16px; padding: 0; display: flex; flex-direction: column; gap: 6px; } .wslist a { display: flex; align-items: baseline; gap: 10px; padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius); text-decoration: none; color: inherit; background: var(--paper); } .wslist a:hover { border-color: var(--accent); } .wsname { font-weight: 600; font-size: 13px; } .wslist .grow, .wslist .hint { margin-left: auto; font-size: 11px; } .rolechip { font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-soft); border: 1px solid var(--line); border-radius: 999px; padding: 1px 7px; } /* The workspace name sits where the tagline used to, next to the wordmark. */ .wsbutton { background: none; border: 1px solid transparent; border-radius: var(--radius); padding: 4px 8px; font: inherit; font-size: 12px; font-weight: 600; color: var(--ink); cursor: pointer; white-space: nowrap; } .wsbutton:hover { border-color: var(--line); background: var(--paper); } .sectionhead { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-soft); margin: 20px 0 6px; } .sectionhead.danger { color: var(--danger); } .rowend { display: flex; gap: 8px; justify-content: flex-end; align-items: center; } .notice { border: 1px solid var(--line); border-radius: var(--radius); padding: 10px; margin-top: 12px; display: grid; gap: 6px; } .notice input, .notice select { width: 100%; border: 1px solid var(--line); border-radius: var(--radius); padding: 6px 8px; font-size: 11.5px; } /* A row of a file, shown to be recognised rather than read. */ .notice select { font-family: 'IBM Plex Mono', monospace; } /* ---- toasts ---- */ .toasts { position: fixed; bottom: 54px; left: 50%; transform: translateX(-50%); z-index: 60; display: flex; flex-direction: column; gap: 8px; align-items: center; pointer-events: none; } .toast { background: var(--ink); color: #eaf0f6; border-radius: 6px; padding: 8px 14px; font-size: 12.5px; box-shadow: var(--shadow-pop); display: flex; align-items: center; gap: 10px; max-width: 560px; pointer-events: auto; } .toast.error { background: var(--danger); color: #fff; } .toast.success { background: var(--rel); color: #fff; } .toast button { color: inherit; opacity: 0.7; font-size: 14px; line-height: 1; } .toast button:hover { opacity: 1; } /* ---- motion + responsive ---- */ @media (prefers-reduced-motion: no-preference) { .ae { animation: slidein 0.18s ease-out; } .toast { animation: slidein 0.18s ease-out; } @keyframes slidein { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } } } @media (max-width: 980px) { /* The sidebar is the only route to another table, so it becomes a drawer here rather than disappearing. TopBar's ☰ is what opens it. */ .sidebar-toggle { display: inline-flex; } .side { position: fixed; left: 0; top: 0; bottom: 0; z-index: 40; transform: translateX(-100%); transition: transform 0.18s ease; box-shadow: 8px 0 30px rgba(23, 39, 59, 0.2); } .side.open { transform: translateX(0); } .backdrop { display: block; } .audit { position: fixed; right: 0; top: 0; bottom: 0; z-index: 30; box-shadow: -8px 0 30px rgba(23, 39, 59, 0.2); } } /* ---- the front door ---- */ /* * The marketing page shares the app's palette and type deliberately: the first * thing it promises is that the product looks like this, and a landing page in * a different design language quietly makes that a lie. */ .market { min-height: 100%; background: var(--paper); overflow-y: auto; } .market-top { display: flex; align-items: center; gap: 12px; padding: 16px 28px; border-bottom: 1px solid var(--line); background: var(--panel); position: sticky; top: 0; z-index: 5; } .market-top .wordmark { font-size: 20px; } /* Longhand on purpose: the shorthand here is more specific than the per-section rules below (`.market section` beats `.hero`), so `padding: 0 24px` silently zeroed every section's vertical rhythm and the whole page ran together. Set only the axis this rule is actually about. */ .market section { max-width: 940px; margin: 0 auto; padding-left: 24px; padding-right: 24px; } .hero { padding-top: 72px; padding-bottom: 48px; text-align: center; } /* * The one line on this page that never varies — see `shared/src/voice.ts`. * * Small, quiet, and above the headline. It is not competing with the headline * for attention; it is there so that a visitor who reads only the top of the * page still learns what the product practically is, whichever story the * sampler drew underneath it. Quiet, but never `--ink-soft`: this is the line * that has to survive being skimmed. */ .hero .strapline { font-size: 13px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-soft); margin: 0 0 14px; } .hero h1 { font-size: 40px; line-height: 1.15; letter-spacing: -0.02em; margin: 0 0 18px; /* The pool's headlines run from four words to a full sentence, so this has to look deliberate at either length rather than only at the current one. */ max-width: 780px; margin-inline: auto; } .hero .lede { font-size: 16.5px; line-height: 1.6; color: var(--ink-soft); max-width: 640px; margin: 0 auto 28px; } .hero-cta { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-bottom: 14px; } .btn.big { padding: 11px 22px; font-size: 14.5px; } .hero .hint { color: var(--ink-soft); font-size: 12.5px; } .proof { padding-bottom: 56px; } /* * Wider than the prose column, because the screenshot is doing the work here * and 940px squeezes the SQL strip past reading. `section.proof` rather than * `.proof` on purpose: `.market section` above carries an element in its * selector and would otherwise win — the same specificity trap that once * flattened this page's vertical rhythm. */ .market section.proof { max-width: 1180px; } /* The caption is prose again, so it goes back to a prose measure. */ .proof .hint { max-width: 940px; } .proof-label { font-size: 10px; letter-spacing: 0.09em; text-transform: uppercase; color: var(--ink-soft); margin-bottom: 8px; } /* * The product shot. `height: auto` with the intrinsic width/height on the tag * keeps the space reserved while it loads, so the page below it does not jump. * The border matters more than it looks: the app's own background is nearly the * page's, and without an edge the screenshot bleeds into the page and stops * reading as a window onto something. */ .shot { display: block; width: 100%; height: auto; border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 14px 40px rgba(23, 39, 59, 0.13); margin: 0 0 14px; } .benefits { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 26px 40px; padding-bottom: 64px; } .benefits h2 { font-size: 17px; margin: 0 0 8px; } .benefits p { margin: 0; color: var(--ink-soft); line-height: 1.6; font-size: 14px; } .pricing { padding-bottom: 56px; border-top: 1px solid var(--line); padding-top: 56px; } .pricing h2 { font-size: 26px; margin: 0 0 6px; letter-spacing: -0.01em; } .pricing > .hint { color: var(--ink-soft); margin: 0 0 26px; } .plans { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 18px; } .plan { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 22px; display: flex; flex-direction: column; gap: 12px; } .plan.featured { border-color: var(--cobalt); box-shadow: 0 10px 30px rgba(32, 80, 216, 0.1); } .plan h3 { margin: 0; font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-soft); } .plan .price { font-size: 34px; font-weight: 700; letter-spacing: -0.02em; } .plan .price .per { font-size: 14px; font-weight: 500; color: var(--ink-soft); } .plan .blurb { margin: 0; color: var(--ink-soft); font-size: 13.5px; line-height: 1.5; } .plan ul { margin: 0; padding-left: 18px; display: grid; gap: 6px; font-size: 13.5px; line-height: 1.45; } .plan .limits { font-size: 11.5px; color: var(--ink-soft); border-top: 1px solid var(--line-soft); padding-top: 10px; } .plan .btn { margin-top: auto; text-align: center; } /* * The last word, under the prices. * * Narrower than the prose above it and centred, so it reads as a closing line * rather than as a fifth benefit someone forgot to put in the grid. */ .closer { padding-top: 8px; padding-bottom: 56px; text-align: center; } .closer p { max-width: 560px; margin: 0 auto 22px; font-size: 15.5px; line-height: 1.6; color: var(--ink-soft); } .market-foot { display: flex; align-items: center; gap: 18px; max-width: 940px; margin: 0 auto; padding: 22px 24px 40px; border-top: 1px solid var(--line); font-size: 13px; } .market-foot .wordmark { font-size: 15px; } .market a { color: var(--cobalt); text-decoration: none; } .market a:hover { text-decoration: underline; } .market .btn, .market .btn:hover { text-decoration: none; } .market .btn.primary { color: #fff; } /* A call to action in the top bar is a link, not prose. */ .top a.btn, .top a.btn:hover { text-decoration: none; color: #fff; } /* A visitor's workspace name: a label, not a menu. */ .wsbutton.static { cursor: default; display: inline-flex; align-items: center; gap: 8px; } @media (max-width: 640px) { .hero h1 { font-size: 30px; } .hero { padding-top: 44px; } /* The top bar is a single nowrap row, so its min-content width was setting the width of the document and pushing the whole app sideways. Below this the words fold away and the search box gives up its floor; every control keeps its glyph, its title and its aria-label. */ .top { gap: 6px; padding: 8px 10px; } .top .wordmark { display: none; } .top .lbl { display: none; } .top .initial { display: inline; } /* The workspace keeps its name here even when nothing else does: on a shared account it is the one thing you most need to be sure of. */ .top .wsname { display: inline-block; max-width: 84px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; } .top .wsbutton, .top .iconbtn { padding: 5px 8px; } .searchbox { min-width: 0; flex: 1 1 60px; } } /* ---- starting points ---- */ .field { display: grid; gap: 6px; margin-bottom: 14px; } .fieldlabel { font-size: 11px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-soft); } /* A labelled checkbox row, as in the demo choice when creating a workspace. */ label.checkrow { display: flex; /* Said out loud: `.login-card label` stacks its children in a column at the same specificity, and only the explicit direction here keeps the box on the same line as its text. */ flex-direction: row; align-items: flex-start; gap: 8px; margin: 10px 0 0; cursor: pointer; } .checkrow input[type='checkbox'] { width: auto; margin-top: 3px; flex: none; accent-color: var(--cobalt); } .checkrow .hint { display: block; font-weight: 400; } .checkrow.nested { margin-left: 22px; } /* The empty workspace: the only screen where the offer is the whole page. */ .empty { max-width: 760px; padding: 32px 24px; text-align: center; } .empty h2 { margin: 0 0 8px; font-size: 21px; letter-spacing: -0.01em; } .empty > .hint { margin: 0 auto 22px; max-width: 560px; line-height: 1.55; } .modal textarea { width: 100%; border: 1px solid var(--line); border-radius: var(--radius); padding: 8px 10px; font-size: 12.5px; line-height: 1.5; resize: vertical; background: var(--panel); color: var(--ink); } .modal textarea:focus { outline: 2px solid var(--cobalt); outline-offset: -1px; } .modal input[type='file'] { padding: 6px 0; border: none; } .notice table.plain { margin-top: 8px; } /* ---- the plan ---- */ /* * Only ever on screen when the plan is about to change what someone can do — * see the note in PlanBanner. A permanent ribbon would be an advert. */ .planbar { display: flex; align-items: center; gap: 10px; padding: 7px 16px; font-size: 12.5px; border-bottom: 1px solid var(--line); flex: none; } .planbar.warn { background: var(--amber-soft); color: var(--amber); } .planbar.bad { background: var(--danger-soft); color: var(--danger); } .planbar .linkbtn { color: inherit; font-weight: 600; text-decoration: underline; } .meter { display: grid; gap: 4px; margin-top: 10px; } .meterhead { display: flex; justify-content: space-between; font-size: 11.5px; color: var(--ink-soft); } .meterbar { height: 6px; border-radius: 3px; background: var(--band); overflow: hidden; } .meterfill { height: 100%; background: var(--cobalt); transition: width 0.2s ease; } .meterfill.near { background: var(--amber); } .meterfill.full { background: var(--danger); } @media (prefers-reduced-motion: reduce) { .meterfill { transition: none; } } .modal .plans { margin-top: 14px; } .modal .plan { padding: 14px; gap: 8px; } .modal .plan .price { font-size: 24px; }