Administration has a nav of its own rather than the chat sidebar, and 1.1.0 gave every `.sidebar` the drawer behaviour -- starts closed, slides in -- without giving that one any of the drawer's furniture. No id for the toggle to resolve, no toggle, no close, no scrim: it sat at left:-280 with nothing in the application able to open it. The close button and the scrim are partials now, used by both, and the test that guards it *finds* sidebars by scanning the templates rather than working from a list, which is exactly why this one was missed. The chat, measured at 390px, spent forty pixels of side padding and a forty-four pixel avatar column before drawing a word -- close to a quarter of the screen on margin, so anything that could not wrap had to be reached sideways. Padding halved and the avatar moved above the turn; a code block gained about sixty pixels. Worse in the same row: `.topbar__actions` asked for 317px of a 390px bar, because the control that used to give in that row is display:none below a tablet width, so the group went rigid and the title -- flex: 1 -- was squeezed to exactly zero. And `.btn--icon` sets a width with no `flex: none`, so the row shrank the button instead of the text: the sidebar toggle measured eighteen pixels across. The picker gives now, and shows its avatar rather than its name on a phone. Also the instrument, which lied twice more: it could not see horizontal overflow at all, because `.shell` is overflow:hidden and its "is this contained" test therefore answered yes for everything on the page; and run from a copy it resolved `STATIC` to a directory that did not exist, rewrote every asset URL to a dead file:// path and reported the whole application overflowing by thirty thousand pixels. It resolves from the imported package now and asserts that what it rewrote to is really there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
463 lines
18 KiB
CSS
463 lines
18 KiB
CSS
/*
|
|
Design tokens.
|
|
|
|
Every colour, space and radius in the application resolves through a variable
|
|
declared here. Component CSS must never hard-code a hex value -- that is what
|
|
makes adding a theme a matter of writing one new block rather than auditing
|
|
every stylesheet.
|
|
|
|
Themes are selected with data-theme on <html>. `moria` is the default and is
|
|
declared on :root so the page is styled even before the theme script runs.
|
|
*/
|
|
|
|
:root {
|
|
/* --- Type ------------------------------------------------------------- */
|
|
--font-display: "Iowan Old Style", "Palatino Linotype", Palatino, Palladio,
|
|
"URW Palladio L", "Book Antiqua", Baskerville, Georgia, serif;
|
|
--font-body: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
|
|
Arial, sans-serif;
|
|
--font-mono: ui-monospace, "SF Mono", "JetBrains Mono", "Fira Code",
|
|
"Cascadia Code", Menlo, Consolas, monospace;
|
|
|
|
--text-xs: 0.75rem;
|
|
--text-sm: 0.8125rem;
|
|
--text-base: 0.9375rem;
|
|
--text-md: 1rem;
|
|
--text-lg: 1.125rem;
|
|
--text-xl: 1.375rem;
|
|
--text-2xl: 1.75rem;
|
|
|
|
--leading-tight: 1.25;
|
|
--leading-normal: 1.6;
|
|
--leading-relaxed: 1.75;
|
|
|
|
/* --- Space (4px scale) ------------------------------------------------ */
|
|
--sp-1: 0.25rem;
|
|
--sp-2: 0.5rem;
|
|
--sp-3: 0.75rem;
|
|
--sp-4: 1rem;
|
|
--sp-5: 1.25rem;
|
|
--sp-6: 1.5rem;
|
|
--sp-8: 2rem;
|
|
--sp-10: 2.5rem;
|
|
--sp-12: 3rem;
|
|
|
|
/* --- Radius & shadow -------------------------------------------------- */
|
|
--radius-sm: 4px;
|
|
--radius: 8px;
|
|
--radius-md: 10px;
|
|
--radius-lg: 12px;
|
|
--radius-xl: 18px;
|
|
--radius-full: 999px;
|
|
|
|
/*
|
|
--- Controls ----------------------------------------------------------
|
|
Every button, input and select resolves its height from these. That is the
|
|
whole reason things line up: a row of mixed controls has one height, not
|
|
whatever each element's padding and font happened to add up to.
|
|
*/
|
|
--control-h: 2.25rem;
|
|
--control-h-sm: 1.75rem;
|
|
--control-h-lg: 2.75rem;
|
|
--control-px: 0.75rem;
|
|
--control-px-sm: 0.5rem;
|
|
|
|
/* --- Layout ----------------------------------------------------------- */
|
|
--sidebar-width: 17.5rem;
|
|
--inspector-width: 24rem;
|
|
/* Wider than the inspector because the content is not prose: eighty columns
|
|
of --font-mono do not fit in 24rem, and a terminal narrower than eighty
|
|
re-wraps everything a program prints. */
|
|
--terminal-width: 34rem;
|
|
--terminal-width-min: 24rem;
|
|
/* Wider again: a source line is longer than eighty columns once nothing is
|
|
re-wrapping it, and this one holds prose as well. The minimum is 24rem =
|
|
384px and must equal both `data-resize-min` in chat/_canvas.html and the
|
|
lower bound in api/preferences.py:LAYOUT_BOUNDS -- a width outside those
|
|
bounds is silently dropped, so the three are pinned equal by a test. */
|
|
--canvas-width: 40rem;
|
|
--canvas-width-min: 24rem;
|
|
--thread-max-width: 48rem;
|
|
--header-height: 3.5rem;
|
|
|
|
/* What `--header-height` does at the top of the shell, this does at the
|
|
bottom. The sidebar's footer and the composer sit either side of the same
|
|
vertical line and both are content-sized, so without this they end at
|
|
different heights -- and neither can be made to match the other by
|
|
accident: the footer's height depends on which entries the reader's
|
|
permissions allow, and the composer's on how much they have typed.
|
|
|
|
This began as the fix for a broken *line*: both carried a top border, and
|
|
the two met the sidebar's edge at different heights. The borders are gone
|
|
now -- an edge that content scrolls under reads better undrawn than drawn
|
|
and aligned -- and the token stays, because the two ends of the shell
|
|
sitting at different heights is visible without any border to prove it.
|
|
|
|
A calc of the pieces the footer is actually built from -- four rows at
|
|
`--control-h`, the gaps between them, and its own padding -- so it stays
|
|
true if those tokens move. Applied as a `min-height` to both: it holds the
|
|
footer at full height even for somebody who sees fewer entries, and lifts
|
|
the composer to meet it. A composer that grows past this as somebody types
|
|
is expected; the input is getting bigger, and nothing is pretending the
|
|
sidebar should follow it. */
|
|
--footer-height: calc(4 * var(--control-h) + 3 * var(--sp-1) + 2 * var(--sp-2));
|
|
|
|
/* The terminal's own type. xterm holds this as a number rather than reading
|
|
it from CSS, so terminal.js parses it back out -- it must stay a plain
|
|
pixel value. */
|
|
--terminal-font-size: 13px;
|
|
|
|
/*
|
|
--- Stacking ----------------------------------------------------------
|
|
These were four bare numbers scattered across two stylesheets, which is
|
|
fine until something new has to sit between two of them and nobody can
|
|
say what is already there.
|
|
*/
|
|
--z-raised: 5;
|
|
--z-handle: 10;
|
|
--z-dropdown: 30;
|
|
--z-panel: 40;
|
|
--z-toast: 60;
|
|
|
|
--transition-fast: 120ms ease;
|
|
--transition: 200ms ease;
|
|
|
|
/* --- Borders -----------------------------------------------------------
|
|
A hairline was a literal `1px` in about ninety places, which made it the
|
|
largest category of hard-coded value left in the codebase -- and the one
|
|
thing a theme cannot currently change. */
|
|
--border-w: 1px;
|
|
--border-w-thick: 2px;
|
|
--border-w-accent: 3px;
|
|
|
|
/* The focus outline's own width. Not `--border-w-thick`, though they are the
|
|
same number today: an outline is drawn outside the box and takes no space,
|
|
a border is part of the box and does. Making one of them follow the other
|
|
means a theme that wants a heavier border gets a heavier focus ring too,
|
|
which is two decisions tied together by a coincidence. */
|
|
--outline-w: 2px;
|
|
|
|
/* --- Touch --------------------------------------------------------------
|
|
A control a thumb has to hit is 44px. `--control-h` is 2.25rem, which is
|
|
36 -- comfortable with a pointer and under every published minimum for a
|
|
finger -- so the coarse-pointer block at the foot of this file raises the
|
|
control tokens to this rather than patching components one at a time.
|
|
Raising the token is the only version that reaches all of them, and it is
|
|
what `--control-h` exists for. */
|
|
--tap-min: 2.75rem;
|
|
/* A tick box, which does not take its size from `--control-h`: the browser
|
|
draws it and only `width`/`height` move it. */
|
|
--check-size: 1rem;
|
|
|
|
/* --- The window's own edges ---------------------------------------------
|
|
Installed on a phone, the page runs under the notch and the home
|
|
indicator: base.html asks iOS for `black-translucent`, which is what puts
|
|
it there, and `viewport-fit=cover` is what lets these resolve to anything
|
|
but zero. Declared here so no component spells `env()` out -- and so a
|
|
desktop browser, where all four are 0, costs nothing. */
|
|
--safe-top: env(safe-area-inset-top, 0px);
|
|
--safe-right: env(safe-area-inset-right, 0px);
|
|
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
|
--safe-left: env(safe-area-inset-left, 0px);
|
|
|
|
/* --- Breakpoints --------------------------------------------------------
|
|
A media query cannot read a custom property, so these cannot be *used*
|
|
here. They are declared anyway so the numbers have one home and a grep for
|
|
one lands somewhere that says what it means -- and
|
|
`tests/test_layout_bounds.py` refuses a width in any stylesheet that is not
|
|
declared here, so a fourth breakpoint invented in passing fails the suite
|
|
rather than joining the set unannounced.
|
|
|
|
--bp-admin 44rem 704px a two-column reference row stacks
|
|
--bp-narrow 48rem 768px the sidebar becomes a drawer, and controls
|
|
grow to a thumb's size
|
|
--bp-wide 64rem 1024px the right-hand panels become overlays */
|
|
--bp-admin: 44rem;
|
|
--bp-narrow: 48rem;
|
|
--bp-wide: 64rem;
|
|
|
|
/* --- Motion -------------------------------------------------------------
|
|
Durations and curves, so the `prefers-reduced-motion` block at the foot of
|
|
this file keeps covering everything by construction: a literal `1.6s` in a
|
|
component is a value that block can still neutralise, but one nobody can
|
|
tune. `--ease-out` is the one to reach for -- something arriving should
|
|
decelerate; `--ease-spring` overshoots slightly and belongs on a thing
|
|
that appears, never on a thing that moves under the pointer. */
|
|
--ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
--ease-in-out: cubic-bezier(0.65, 0.05, 0.36, 1);
|
|
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
--dur-1: 120ms;
|
|
--dur-2: 200ms;
|
|
--dur-3: 320ms;
|
|
--dur-slow: 1.6s;
|
|
|
|
/* --- Panel minimums -----------------------------------------------------
|
|
`api/preferences.py:LAYOUT_BOUNDS` allows four panels' widths to be stored
|
|
against an account and only two of them -- the two with a drag handle --
|
|
had a `-min` token or a `min-width` to clamp with. The other two are not
|
|
draggable, so nothing in the interface could produce a bad value; but the
|
|
endpoint takes one from anybody signed in, `base.html` applies stored
|
|
widths to <html> before first paint, and with no clamp a stored 800px
|
|
sidebar is one nothing in the application can drag back. */
|
|
--sidebar-width-min: 12.5rem;
|
|
--inspector-width-min: 17.5rem;
|
|
|
|
/* The focus treatment, written once. Three components spelled it out. It
|
|
resolves --accent-soft at the point of use, so it follows the theme even
|
|
though it is declared above them. */
|
|
--ring: 0 0 0 3px var(--accent-soft);
|
|
}
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
MORIA (default, dark)
|
|
|
|
Deep stone and lamplight: the halls under the mountain. Surfaces are cool and
|
|
near-neutral so the mallorn and mithril accents carry all the colour.
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
:root,
|
|
:root[data-theme="moria"] {
|
|
color-scheme: dark;
|
|
|
|
--bg: #101317;
|
|
--bg-sunken: #0B0E11;
|
|
--surface: #171B21;
|
|
--surface-raised: #1E242B;
|
|
--surface-hover: #232A32;
|
|
--surface-active: #2A323B;
|
|
|
|
--border: #2A313A;
|
|
--border-strong: #3A434E;
|
|
|
|
--ink: #E4E8EC;
|
|
--ink-muted: #A2ADB8;
|
|
--ink-faint: #7A848F;
|
|
--ink-inverse: #0B0E11;
|
|
|
|
/* Mithril: the cool primary, used for focus and interactive accents. */
|
|
--accent: #8FB3CC;
|
|
--accent-hover: #A9C6DA;
|
|
--accent-ink: #0B0E11;
|
|
--accent-soft: rgba(143, 179, 204, 0.14);
|
|
|
|
/*
|
|
Mallorn: the brand accent, and the assistant's mark. A yellow-leaning leaf
|
|
green, so it stays warm against the mithril blue rather than turning the
|
|
palette into two cool accents that compete.
|
|
*/
|
|
--leaf: #9BCC5A;
|
|
--leaf-hover: #B1DD74;
|
|
--leaf-soft: rgba(155, 204, 90, 0.14);
|
|
|
|
/* Ember: destructive actions and errors. */
|
|
--danger: #E2795A;
|
|
--danger-hover: #EC8E72;
|
|
--danger-soft: rgba(226, 121, 90, 0.14);
|
|
|
|
/*
|
|
Success leans teal rather than leaf. Two greens a hue apart read as one
|
|
colour rendered inconsistently -- an unread dot beside a brand badge has to
|
|
be tellable from it at a glance.
|
|
*/
|
|
--success: #5FBFA0;
|
|
--success-soft: rgba(95, 191, 160, 0.14);
|
|
--warning: #DFAE58;
|
|
--warning-soft: rgba(223, 174, 88, 0.14);
|
|
|
|
--bubble-user: #232B34;
|
|
--bubble-assistant: transparent;
|
|
--code-bg: #0C0F13;
|
|
--code-border: #262D36;
|
|
|
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
|
|
--shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
|
|
--shadow-lg: 0 12px 34px rgba(0, 0, 0, 0.55);
|
|
|
|
--scrim: rgba(6, 8, 10, 0.66);
|
|
|
|
/*
|
|
The sixteen ANSI colours, for the terminal panel.
|
|
|
|
A shell picks its own colours -- `ls --color`, a git diff, htop's meters --
|
|
and until these existed it got xterm's defaults, which are a different
|
|
palette from this one and read as a foreign window pasted into the
|
|
application. Where a slot has an obvious counterpart above it takes it, so
|
|
an error in the terminal is the same ember as an error anywhere else.
|
|
|
|
"black" is not #000: it is what a program picks for a dim background or
|
|
faint text, and on a near-black surface a true black is invisible.
|
|
*/
|
|
--ansi-black: #1A1F26;
|
|
--ansi-red: #E2795A;
|
|
--ansi-green: #9BCC5A;
|
|
--ansi-yellow: #DFAE58;
|
|
--ansi-blue: #8FB3CC;
|
|
--ansi-magenta: #C08FCC;
|
|
--ansi-cyan: #5FBFA0;
|
|
--ansi-white: #C6CDD4;
|
|
--ansi-bright-black: #4A535E;
|
|
--ansi-bright-red: #EC8E72;
|
|
--ansi-bright-green: #B1DD74;
|
|
--ansi-bright-yellow: #EFC87A;
|
|
--ansi-bright-blue: #A9C6DA;
|
|
--ansi-bright-magenta: #D4A9DE;
|
|
--ansi-bright-cyan: #7FD4BA;
|
|
--ansi-bright-white: #E4E8EC;
|
|
}
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
SHIRE (light)
|
|
|
|
Parchment, ink and moss: warm, low-contrast, easy to read for a long time.
|
|
Backgrounds are deliberately off-white -- pure #FFF next to the leaf accent
|
|
reads as clinical rather than as paper.
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
/*
|
|
`[data-base="shire"]` as well as `[data-theme="shire"]`, and that second
|
|
selector is what makes a custom theme possible. A custom theme is a handful of
|
|
token overrides served from /branding.css; everything it does not set has to
|
|
come from somewhere, and Moria's block above matches bare `:root` so it always
|
|
applies. Without this a custom *light* theme would be four light colours on
|
|
near-black surfaces. `<html>` carries both attributes -- see base.html and
|
|
app.js:applyTheme.
|
|
*/
|
|
:root[data-theme="shire"],
|
|
:root[data-base="shire"] {
|
|
color-scheme: light;
|
|
|
|
--bg: #F6F1E4;
|
|
--bg-sunken: #EDE6D4;
|
|
--surface: #FDFBF5;
|
|
--surface-raised: #FFFFFF;
|
|
--surface-hover: #F1EADA;
|
|
--surface-active: #E7DEC9;
|
|
|
|
--border: #DED3BB;
|
|
--border-strong: #C6B896;
|
|
|
|
--ink: #2C2419;
|
|
--ink-muted: #6A5C48;
|
|
--ink-faint: #756A55;
|
|
--ink-inverse: #FDFBF5;
|
|
|
|
/* Hobbit-door blue-green: the cool primary. */
|
|
--accent: #3E6B7A;
|
|
--accent-hover: #325867;
|
|
--accent-ink: #FDFBF5;
|
|
--accent-soft: rgba(62, 107, 122, 0.12);
|
|
|
|
/* The same mallorn, darkened until it holds its own as text on parchment. */
|
|
--leaf: #4C7A22;
|
|
--leaf-hover: #3C6318;
|
|
--leaf-soft: rgba(76, 122, 34, 0.13);
|
|
|
|
--danger: #A6432B;
|
|
--danger-hover: #8C3722;
|
|
--danger-soft: rgba(166, 67, 43, 0.11);
|
|
|
|
--success: #2C7360;
|
|
--success-soft: rgba(44, 115, 96, 0.12);
|
|
--warning: #98701A;
|
|
--warning-soft: rgba(152, 112, 26, 0.13);
|
|
|
|
--bubble-user: #EDE4CF;
|
|
--bubble-assistant: transparent;
|
|
--code-bg: #F2EBD9;
|
|
--code-border: #DED3BB;
|
|
|
|
--shadow-sm: 0 1px 2px rgba(72, 58, 34, 0.09);
|
|
--shadow: 0 4px 14px rgba(72, 58, 34, 0.11);
|
|
--shadow-lg: 0 12px 34px rgba(72, 58, 34, 0.16);
|
|
|
|
--scrim: rgba(44, 36, 25, 0.4);
|
|
|
|
/*
|
|
The same sixteen on parchment, and the reason they are not the Moria set
|
|
lightened: every one of these has to be readable as *text* on #F2EBD9, so
|
|
the whole palette is darkened rather than brightened. "bright" therefore
|
|
means more emphatic here, not lighter -- a light terminal theme that made
|
|
bright yellow actually bright would render it invisible.
|
|
*/
|
|
--ansi-black: #2C2419;
|
|
--ansi-red: #A6432B;
|
|
--ansi-green: #4C7A22;
|
|
--ansi-yellow: #98701A;
|
|
--ansi-blue: #3E6B7A;
|
|
--ansi-magenta: #7A3E6B;
|
|
--ansi-cyan: #2C7360;
|
|
--ansi-white: #6A5C48;
|
|
--ansi-bright-black: #94856D;
|
|
--ansi-bright-red: #8C3722;
|
|
--ansi-bright-green: #3C6318;
|
|
--ansi-bright-yellow: #7A5A14;
|
|
--ansi-bright-blue: #325867;
|
|
--ansi-bright-magenta: #63325A;
|
|
--ansi-bright-cyan: #235C4D;
|
|
--ansi-bright-white: #453A2A;
|
|
}
|
|
|
|
/*
|
|
--- Touch -----------------------------------------------------------------
|
|
A pointer is precise and a finger is about 9mm across, so the same control
|
|
cannot be the right size for both. `--control-h` is 36px, which is comfortable
|
|
with a mouse and under every published minimum for a thumb; `--control-h-sm`
|
|
is 28px, which is a target most people miss.
|
|
|
|
Raised here rather than patched per component, because there are upwards of
|
|
forty of them and the next one added would be 36px again. `--control-h` is
|
|
what every button, input and select resolves its height from, so one block
|
|
moves all of them -- which is the reason that token exists.
|
|
|
|
Two conditions, either of which is enough.
|
|
|
|
`(pointer: coarse)` is the honest one: it is the input device that decides how
|
|
big a target has to be, and a touchscreen laptop at 1440px has the same thumb
|
|
as a phone. But a layout below the phone breakpoint is a one-column, drawer-
|
|
navigated layout whatever is pointing at it -- there is room for bigger
|
|
controls and every reason to use it -- and that half is also the half a
|
|
headless browser can be made to prove, which is not nothing: a rule that can
|
|
only be checked by holding a phone is a rule that quietly rots.
|
|
*/
|
|
@media (pointer: coarse), (max-width: 48rem) {
|
|
:root {
|
|
--control-h: var(--tap-min);
|
|
/* 40px, not the 36 a comfortable pointer gets. A `.btn--sm` is a secondary
|
|
action, not an unimportant one -- Edit, Enable and Use default are all
|
|
`.btn--sm`, and on a phone they are the whole interaction. */
|
|
--control-h-sm: 2.5rem;
|
|
--control-px: var(--sp-4);
|
|
--control-px-sm: var(--sp-3);
|
|
/* A native checkbox is 13-16px whatever the surrounding type is, and no
|
|
amount of padding on its label changes the box itself. It is the
|
|
smallest target in the application on a phone by some margin. */
|
|
--check-size: 1.375rem;
|
|
}
|
|
}
|
|
|
|
/* Respect a stated preference for reduced motion everywhere, at once. */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
scroll-behavior: auto !important;
|
|
}
|
|
/* The motion tokens too, for anything that composes a duration rather than
|
|
declaring one -- a `transition: transform var(--dur-3)` is neutralised by
|
|
the rule above, but an `animation-delay` built from one is not. */
|
|
:root {
|
|
--dur-1: 0.01ms;
|
|
--dur-2: 0.01ms;
|
|
--dur-3: 0.01ms;
|
|
--dur-slow: 0.01ms;
|
|
--transition-fast: 0.01ms;
|
|
--transition: 0.01ms;
|
|
--transition-slow: 0.01ms;
|
|
}
|
|
}
|