Skip to content

UI audit — hyperbolic theme, 2026-08-10

Twelve defects found by reading seventeen screenshots of the running app. Each entry: what's wrong, the evidence, the law it breaks (see STYLEGUIDE.md), and the fix. Fixes are implemented in static/css/hyperbolic-fixes.css — land that file, then work this list to confirm each one is actually resolved in the browser.

Ordered by leverage. The first three account for most of what makes the theme look unfinished.


D1 — Native controls are painted in dark mode ★ root cause of 3 defects

Evidence. /content/new: every axis checkbox renders as a solid black square. /connecting/{id}: the date input renders as a near-black fill with white text, on a white card. /mira: the transcript scrollbar is a solid black bar down the middle of the page.

Cause. The dark default sets color-scheme: dark (or inherits it), and the hyperbolic scope never overrides it. The browser therefore paints every UA-drawn control — checkbox, radio, date picker, scrollbar, select arrow — with its dark-mode palette, regardless of your CSS.

Fix. One declaration in the theme scope: color-scheme: light; accent-color: var(--accent); This is a token-level fix, so it belongs in the theme block and violates nothing. Verify all three surfaces after landing it.


D2 — The header has its own gradient, and it doesn't match the void

Evidence. Every screenshot. The nav bar runs a horizontal pink → mint gradient; the page below runs the pearl-void radials. They meet at a hard horizontal seam about 100px down, visible across the full width.

Law broken. Law 3 — the only sanctioned gradients are the --grad-* tokens and the pearl-void backdrop. The header gradient is neither.

Fix. The header is chrome sitting in front of the void: give it --panel-fill-recessed plus backdrop-filter, no gradient of its own. The void then reads continuously behind it and the seam disappears.


D3 — Content sits directly on the raw gradient

Evidence. /content/new, /connecting, /focus, /news, /writings: body copy, labels, and form fields sit on the void with no surface beneath them. On /content/new the right third of the page is an unbroken peach wash with nothing on it, while the form's left edge sits over the lavender band — so identical inputs read as different colours depending on where they land on the page.

Law broken. Law 3's "never behind text", and the glass hierarchy: the void is atmosphere, panels are what content sits on.

Fix, in two parts. 1. Add a scrim to the void stack — a rgba(255,255,255,0.55) layer over the radials, so even bare text has an even field behind it. The iridescence survives; the saturation stops competing with type. 2. Give the page's main column a glass panel (--panel-fill + --panel-blur + --panel-border + --shadow-2). Content should never be a direct child of the void.


D4 — Form fields are translucent, so they tint with the background

Evidence. /content/new: the Title, Duration, and Notes fields render as pale lavender/pink washes that shift hue across the page. The Notes textarea is visibly pinker than the Title input above it.

Law broken. The legibility carve-out — work surfaces stay opaque. An input the user types into is a work surface.

Fix. --input-bg: var(--surface) — opaque white — in the theme scope. Inputs keep the pale border and the 14px radius; they stop being windows.


D5 — The dark theme's amber leaks into the active nav underline

Evidence. Every screenshot: the active nav item (Today / Connect / Ingest) carries an orange-amber underline while every other accented element on the page — links, buttons, eyebrows, the wordmark — is magenta.

Cause. A hardcoded amber, or a rule referencing the dark accent directly rather than var(--accent).

Fix. var(--accent). The style gate's hex check catches this class of bug once it runs.


D6 — The "+ New conversation" button is a gold gradient

Evidence. /mira, conversation rail. A gold → orange gradient fill on the primary button of a magenta-accent page. It is the only gold element in the entire app.

Law broken. Law 3 (unsanctioned gradient) and, separately, accent consistency.

Fix. Solid var(--accent), matching every other primary button ("Log it", "Activate", "Save draft", "Generate this week's prompts").


D7 — The Mira transcript is clipped and overlaps the composer

Evidence. /mira: the last visible line — "distinguishes this from the obvious alternative?" — is cut horizontally mid-glyph, with the composer box drawn over it. The scroll container's height doesn't resolve against its parent.

Cause. Classic flex/grid overflow: a scrollable child inside a flex column needs min-height: 0 (or min-block-size: 0) or it refuses to shrink and spills under its sibling.

Fix. min-height: 0 on the transcript's flex parent, overflow-y: auto on the transcript, and let the composer be a fixed-height sibling. Also scroll-padding-block-end so the newest turn isn't tucked under the composer's shadow.


D8 — The content column is not centred

Evidence. /content/new at a ~1650px viewport: content spans roughly x=418 to x=1138, centre ≈778, while the viewport centre is ≈880. About 100px left of centre. Same on /library and /news.

Cause. A max-width container without margin-inline: auto, or an asymmetric padding that only compensates for a scrollbar on one side.

Fix. margin-inline: auto on the container and scrollbar-gutter: stable both-edges on the scroll root, so the layout doesn't shift when a scrollbar appears.


D9 — Pages with little content are 70% empty gradient

Evidence. /focus (one initiative row), /connecting (one contact), /writings (one draft). Content occupies the top quarter; the rest is wallpaper.

Not a bug in the theme — it's what an empty state looks like when the theme is loud. Two fixes, both cheap: the scrim in D3 calms the empty region, and these routes need real empty-state content (what this page is for, what to do first) rather than a bare list with one row.


D10 — Card borders are inconsistent

Evidence. /aim: the "Where you stand" card has a magenta 1px border. Every other card in the app has the pale lavender hairline. Nothing about that card is selected or in an error state.

Fix. var(--panel-border). Accent borders mean "selected" or "highlighted" — if the card is meant to be featured, it should say so with a shadow step, not by borrowing the selection colour.


D11 — Two different card radii on the same page

Evidence. /mira: the conversation rail's active item and the panels use ~14px; the outer panels use ~20px; the tab strip's chips are pills. On /library the content rows are square-cornered while their container is 20px.

Fix. --radius-card (20px) for panels, --radius (14px) for controls and inner blocks, --radius-pill for chips and badges. No third value.


D12 — "SOON" badges read as active tabs

Evidence. /mira sub-nav: "KB SOON" and "Voice SOON" sit at the same visual weight as Chat / Agenda / Memory / Evals, so they look clickable.

Fix. Disabled treatment on the parent tab — --text-faint, cursor: not-allowed, aria-disabled="true" — with the badge in --surface-2. A user should be able to tell what's live without clicking.


Verifying the fix

Screenshot these five routes in the hyperbolic theme, before and after: /mira, /content/new, /aim, /connecting/{id}, /focus. Then flip to the dark theme and screenshot the same five — the dark theme must be pixel-identical to its current state, since every fix here is either a token redefinition inside the hyperbolic scope or a layout bug that was wrong in both themes (D7, D8).