diff --git a/.cursor/rules/dark-theme-no-banding.mdc b/.cursor/rules/dark-theme-no-banding.mdc
new file mode 100644
index 0000000..b5e8d95
--- /dev/null
+++ b/.cursor/rules/dark-theme-no-banding.mdc
@@ -0,0 +1,49 @@
+---
+description: Dark-theme rules that prevent 8-bit colour banding (contour lines)
+globs: src/styles/**/*.css
+alwaysApply: false
+---
+
+# Dark surfaces must not band
+
+Contour lines in a dark UI come from 8-bit quantization: sRGB has very few code
+levels near black, so a wide soft gradient has nothing to put between its steps
+and renders as flat plateaus with hard edges. Below the sRGB toe brightness is
+proportional to code value, so **one step at value 7 is a ~14% brightness jump;
+at value 21 it is ~5%** and stops reading as a line. Full explanation in
+`README.md` → "Dark UI without colour banding".
+
+## Rules
+
+1. **A gradient that crosses more pixels than it has code levels is not a
+ gradient — it is plateaus with visible edges.** Use a flat fill.
+2. **Never take a surface below `--bg-0` (`#0b111c`).** Vignettes, scrims and
+ overlays must land above that floor too, or they drag their region back into
+ the crush zone.
+3. **Gradients on dark surfaces must be small and steep** — a 40px button fade is
+ fine, a 900px wash is not.
+4. **Depth = one step of fill tone + spacing.** Not washes, not outlines. Surfaces
+ nest by tone (background → panel → card) and carry no border; borders are only
+ for things that float (menus, popovers).
+5. **One light source.** Only the reactor canvas glows. Note the trap: on a flat
+ background a lone soft halo becomes the only brightness variation, which is
+ exactly what vignetting looks like.
+
+```css
+/* ❌ BAD — 2 levels of green stretched over 860px, plus window-sized washes */
+background:
+ radial-gradient(92% 78% at 50% 30%, rgba(18, 36, 68, 0.55), transparent 74%),
+ linear-gradient(180deg, #05070d 0%, #030509 100%);
+
+/* ✅ GOOD */
+background: var(--bg-1);
+```
+
+## Do not "fix" banding with these
+
+- **Noise/dither overlays.** Cannot remove a staircase already baked into a
+ rasterized layer, and `backdrop-filter` averages any dither beneath it away, so
+ banding returns inside blurred glass.
+- **`mix-blend-mode: overlay`.** Resolves to `2 × backdrop × source` below
+ mid-grey, so on dark surfaces its effect is a fraction of one code level.
+- **More gradient stops.** The limit is quantization, not stop count.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3317d96..13a5edd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,53 @@ and the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. Iris is
pre-1.0, so minor versions may still contain breaking changes to configuration
and IPC surfaces.
+## [0.4.0] — 2026-07-25
+
+The Luminous Instrument redesign. The interface is rebuilt on flat surfaces and a
+real token system, which also eliminates the colour banding that dark UIs
+normally suffer from.
+
+### Changed
+
+- **The UI is rebuilt on flat surfaces.** The deck, HUD, and overlays no longer
+ use stacked gradient washes. Depth now comes from one step of fill tone plus
+ spacing rather than from washes and outlines, and nested 1px borders are gone
+ from panels, bubbles, and cards — edges are kept only for floating menus.
+- **The palette is lifted off near-black**, from `#030509` to `#0b111c`. This is
+ the core fix for contour lines: below the sRGB toe, brightness is proportional
+ to code value, so one code step at value 7 is a ~14% brightness jump while at
+ value 21 it is ~5% and stops reading as an edge.
+- **Tokens are retuned** into proper type, space, radius, and elevation scales,
+ and the reactive accent now tracks the orb state so ambient light follows what
+ Iris is doing.
+- **Light only exists while the reactor runs.** The canvas halo carried a fixed
+ alpha floor, so a sleeping orb still emitted a glow disc wider than its drawn
+ ring, which the asleep filter turned into a grey smudge. The halo is now gated
+ on how far energy sits above rest, keyed off a single shared constant, with
+ stops rebalanced so the awake look is unchanged.
+- **View → Toggle Full Screen** is replaced by **Toggle Glass HUD**. The old item
+ was a no-op on this frameless window; the HUD now has a menu-bar exit.
+
+### Fixed
+
+- **Colour banding in dark areas.** The visible contour lines were 8-bit
+ quantization, not a rendering bug. A soft gradient spread across ~900px has
+ fewer code levels than pixels to cross, so it renders as wide flat plateaus
+ with hard 1-level edges. It only showed in dim states because the aurora and
+ glow were dimmed there, leaving the near-flat base ramp as the only structure
+ on screen. The six-wash drifting aurora, the full-window vignette, and the
+ window-sized deck and boot gradients are now flat fills.
+- **Phantom depth around the orb.** The dark radial scrim behind the HUD orb and
+ the plinth under every orb stage are gone. Both were soft washes standing in
+ for depth, and on a flat dark surface they read as shadow rather than light.
+
+### Added
+
+- **"Dark UI without colour banding"** in the README — a full writeup of the
+ cause and the five rules Iris follows, since the problem is general to dark
+ interfaces and the analysis transfers to any project.
+- A Cursor rule scoped to `src/styles` that enforces those rules for future work.
+
## [0.3.0] — 2026-07-25
The all-day release. Iris gains a searchable knowledge graph, survives a full
@@ -144,5 +191,6 @@ First public release.
- macOS packaging and production launch support.
- README showcase with demo video and screenshots.
+[0.4.0]: https://github.com/ASHR12/iris/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/ASHR12/iris/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/ASHR12/iris/releases/tag/v0.2.0
diff --git a/README.md b/README.md
index 0521e51..52d5352 100644
--- a/README.md
+++ b/README.md
@@ -98,7 +98,7 @@ When Hermes finishes a background task, Iris **proactively speaks up**: *"Quick
### Design
-- **Deep-space design system** — electric cyan + violet on near-black, glass panels, Inter / Space Grotesk / JetBrains Mono
+- **Deep-space design system** — electric cyan + violet on flat deep navy (deliberately *not* near-black — see [below](#dark-ui-without-colour-banding)), glass panels, Inter / Space Grotesk / JetBrains Mono
- **The orb** — a canvas-drawn arc reactor that breathes with the actual audio level, changes palette per state (listening / speaking / working), and flashes when work is handed off
- **Orb micro-expressions** — a double-pulse on wake, a soft ripple when your words are locked in, and an orbiting "thinking" swirl in the gap before Iris speaks
- **Two voice signatures** — your voice renders as sharp radial bars around the orb; Iris's voice as a smooth breathing wave. You can *see* who's talking.
@@ -106,6 +106,75 @@ When Hermes finishes a background task, Iris **proactively speaks up**: *"Quick
- **Comet handoff** — a particle streaks from the orb to the task card when Gemini delegates, and back when Hermes returns
- **Cinematic boot sequence**, animated transitions, and a custom app icon rendered from the orb itself
+### Dark UI without colour banding
+
+If you have ever built a dark interface and seen faint **contour lines** — concentric
+rings or horizontal bands across large dark areas — this section is why Iris does
+not have them. It is the most transferable thing in this design system, so it is
+written up in full.
+
+**The cause.** A display gives you 256 steps per channel, and sRGB spends very few
+of them near black. A soft gradient stretched across 900px has only a handful of
+values available to cross that distance, so it cannot render as a gradient at all:
+it becomes a few wide flat plateaus with a hard 1-level edge between each. Those
+edges are the contour lines. It gets worse the darker you go, because below the
+sRGB toe brightness is proportional to code value — **one code step at value 7 is a
+~14% jump in brightness; at value 21 the same step is ~5%**, which is under the
+threshold where the eye reads it as a line.
+
+This is not a bug anyone has fixed. You can see it in Cursor, in native macOS
+windows, and in dark video. Polished apps don't solve it — they avoid causing it.
+Look closely at Linear, Raycast, Xcode or Things: almost no large soft gradients
+anywhere. Flat surfaces, hairlines, small shadows, and gradients only where they
+are small and steep.
+
+**The rules Iris follows.**
+
+1. **If a gradient crosses more pixels than it has code levels to cross them with,
+ it is not a gradient — it is a stack of plateaus with visible edges.** Make it a
+ flat fill. The deck background was two window-sized radial washes plus a
+ `--bg-1 → --bg-0` ramp; that ramp spanned **2 levels of green over 860px**.
+ Invisible as shading, glaring as edges.
+2. **Keep the whole palette out of the crush zone.** `--bg-0` is `#0b111c`, not the
+ `#030509` it started as. Everything painted later — vignettes, scrims, overlays —
+ has to respect the same floor, or it drags its own region back down.
+3. **Gradients on dark surfaces must be small and steep.** A 40px button fade has
+ plenty of levels for its range. A 900px wash does not.
+4. **Depth comes from a step of fill tone plus spacing**, not from washes or
+ outlines. Surfaces here nest by tone: background → panel → card. None of them
+ carry a border, because a surface a step lighter than its parent already reads as
+ separate, and saying it twice means the lines are what you notice.
+5. **One light source, and let it be real.** The reactor canvas is the only thing
+ that glows. Beware the trap: once the background is flat, a single soft halo
+ becomes the *only* brightness variation, so a bright centre falling off to dark
+ corners is exactly what vignetting looks like.
+
+**What does not work**, in case you are tempted:
+
+- **A noise/dither overlay.** The standard advice, and it is a band-aid. It cannot
+ remove a staircase that is already baked into a rasterized layer — it only
+ textures over it, so the low-frequency steps the eye integrates survive. Worse,
+ `backdrop-filter` **averages** its backdrop, so any dither underneath a blurred
+ glass panel is smoothed away and the banding comes back inside the glass.
+- **Blend modes.** `mix-blend-mode: overlay` resolves to `2 × backdrop × source`
+ below mid-grey, so on a near-black surface its effect is a fraction of one code
+ level. Invisible.
+- **More gradient stops.** The steps are a quantization limit, not a stop-count
+ problem.
+
+**If you need to measure it.** Run-length checks will lie to you: dither makes runs
+look short while the banding is untouched. The eye integrates *along* a long
+straight edge, so average each row across a wide strip of background first (which
+cancels per-pixel noise), then look for jumps in that row-average. Max local slope
+is the number that tracks visibility — a smooth 30-level ramp over 600px climbs at
+~0.05 levels/px, and anything spiking well above that is a line you can see.
+
+> These rules are enforced for future work, not just documented: they live as a
+> Cursor rule in [`.cursor/rules/dark-theme-no-banding.mdc`](.cursor/rules/dark-theme-no-banding.mdc),
+> scoped to `src/styles/**/*.css`, so anyone (or any agent) editing the styles gets
+> them automatically. Copy that file into another project to carry the approach
+> over.
+
---
diff --git a/electron/main.mjs b/electron/main.mjs
index 324e1d9..2176562 100644
--- a/electron/main.mjs
+++ b/electron/main.mjs
@@ -3784,7 +3784,23 @@ function installAppMenu() {
{ role: "forceReload" },
{ role: "toggleDevTools" },
{ type: "separator" },
- { role: "togglefullscreen" },
+ // NOT `role: "togglefullscreen"`. The window is `fullscreenable: false`
+ // (it is transparent and frameless, and the Glass HUD is this app's
+ // real full-screen mode), so that item was a silent no-op. Worse, it
+ // implied a full-screen state the user would then look to undo.
+ // This exposes the actual mode switch, and gives a way back out of the
+ // HUD from the menu bar as well as the tray and ⌥H.
+ // The hotkey is shown in the label rather than set as `accelerator`:
+ // ⌥H is already claimed by globalShortcut, and registering the same
+ // chord in both places risks firing twice — which would toggle the HUD
+ // out and straight back in, making the item look broken.
+ {
+ label: `Toggle Glass HUD (${hudHotkey().replace("Alt+", "⌥")})`,
+ click: () => {
+ toggleHud();
+ updateTrayMenu();
+ },
+ },
],
},
{ role: "windowMenu" },
diff --git a/package-lock.json b/package-lock.json
index 8747a85..c35cd60 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "iris-hermes-voice",
- "version": "0.3.0",
+ "version": "0.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "iris-hermes-voice",
- "version": "0.3.0",
+ "version": "0.4.0",
"license": "MIT",
"dependencies": {
"@fontsource-variable/inter": "^5.2.8",
diff --git a/package.json b/package.json
index 16e9d6c..8a69839 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "iris-hermes-voice",
"productName": "Iris",
- "version": "0.3.0",
+ "version": "0.4.0",
"private": true,
"license": "MIT",
"description": "A futuristic Gemini Live voice companion that delegates real work to Hermes.",
diff --git a/src/App.tsx b/src/App.tsx
index b37ea11..4b59464 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import type { ReactorState, TaskCard, LogLine, TranscriptLine } from "./types";
import {
TERMINAL,
@@ -19,6 +19,7 @@ import TopBar from "./components/TopBar";
import CommsPanel from "./components/CommsPanel";
import CameraDock from "./components/CameraDock";
import CenterStage from "./components/CenterStage";
+import { ORB_ACCENT } from "./components/ReactorCore";
import WorkStream from "./components/WorkStream";
import ReaderOverlay from "./components/ReaderOverlay";
import HistoryDrawer from "./components/HistoryDrawer";
@@ -1499,11 +1500,11 @@ export default function App() {
className={`deck ${sidecarRunning ? "awake" : "asleep"} ${
modeTransition === "to-hud" ? "deck-leaving" : ""
} ${modeTransition === "to-deck" ? "deck-entering" : ""}`}
+ /* The reactor is the lamp of the room: this inherits down so the
+ ambient aurora, glass edge-light and hairlines are all tinted by
+ whatever Iris is doing right now. */
+ style={{ "--orb-accent": ORB_ACCENT[reactorState] } as CSSProperties}
>
-
-
-
-
{!compact ? (
-
+
diff --git a/src/components/CenterStage.tsx b/src/components/CenterStage.tsx
index e7894b8..f3fa149 100644
--- a/src/components/CenterStage.tsx
+++ b/src/components/CenterStage.tsx
@@ -1,19 +1,9 @@
import { useEffect, useState, type CSSProperties, type RefObject } from "react";
import { Mic, MicOff, Power } from "lucide-react";
-import ReactorCore from "./ReactorCore";
+import ReactorCore, { ORB_ACCENT } from "./ReactorCore";
import DevicePicker from "./DevicePicker";
import type { HandoffTone, ReactorState } from "../types";
-// Arc-reactor accent color per state (matches ReactorCore palettes) — drives the
-// surrounding ring/radar so it stays the same color as the orb.
-const ORB_ACCENT: Record
= {
- idle: "120, 170, 150",
- online: "18, 163, 148",
- listening: "40, 205, 170",
- speaking: "238, 122, 92",
- working: "120, 180, 120",
-};
-
function Telemetry({
awake,
gemini,
@@ -158,7 +148,7 @@ export default function CenterStage({
{/* Zoom-style split control: mute toggles, the caret picks the mic. */}
diff --git a/src/components/CommsPanel.tsx b/src/components/CommsPanel.tsx
index c3360d1..f735b97 100644
--- a/src/components/CommsPanel.tsx
+++ b/src/components/CommsPanel.tsx
@@ -22,7 +22,11 @@ export default function CommsPanel({
{transcript.length === 0 ? (
-
No conversation yet. Wake Iris and start talking.
+
+
+
+
No conversation yet
+
Wake Iris and start talking — everything you say lands here.
{testDataEnabled ? (
Load demo comms
diff --git a/src/components/HudShell.tsx b/src/components/HudShell.tsx
index 93ab495..0b90cca 100644
--- a/src/components/HudShell.tsx
+++ b/src/components/HudShell.tsx
@@ -1,20 +1,12 @@
import { useEffect, useRef, useState, type CSSProperties, type RefObject } from "react";
-import { BrainCircuit, ChevronDown, Hand, Maximize2, MessageSquare, Mic, MicOff, Power, Terminal } from "lucide-react";
-import ReactorCore from "./ReactorCore";
+import { BrainCircuit, ChevronDown, Hand, MessageSquare, Mic, MicOff, Minimize2, Power, Terminal } from "lucide-react";
+import ReactorCore, { ORB_ACCENT } from "./ReactorCore";
import WorkCard from "./WorkCard";
import { HandSkeleton } from "./CameraDock";
import type { HandoffTone, ReactorState, TaskCard, TranscriptLine } from "../types";
import type { HandState } from "../hooks/useHandControl";
import { acceptedKey } from "../lib/tasks";
-const ORB_ACCENT: Record = {
- idle: "120, 170, 150",
- online: "18, 163, 148",
- listening: "40, 205, 170",
- speaking: "238, 122, 92",
- working: "120, 180, 120",
-};
-
function HudCamera({
stream,
hand,
@@ -183,7 +175,11 @@ export default function HudShell({
})();
return (
-
+
{/* Slim work stream, top-right — collapsible like Comms */}
{visibleTasks.length > 0 ? (
@@ -263,7 +259,10 @@ export default function HudShell({
{/* One source of truth: App's caption already covers awake states,
asleep hints, and the token-saving nap (with/without Hermes). */}
- {caption}
+ {/* State light, matched to the orb's current color. Suppressed on
+ hints — those are instructions, not Iris speaking. */}
+ {awake && !captionCompact ? : null}
+ {caption}
-
-
+ {/* Inward arrows are the conventional "exit fullscreen" glyph;
+ Maximize2's outward arrows read as "make this bigger", the
+ opposite of what this does. */}
+
+
diff --git a/src/components/ReactorCore.tsx b/src/components/ReactorCore.tsx
index 6849437..9f300e7 100644
--- a/src/components/ReactorCore.tsx
+++ b/src/components/ReactorCore.tsx
@@ -1,6 +1,5 @@
import { useEffect, useRef } from "react";
-
-type ReactorState = "idle" | "online" | "listening" | "speaking" | "working";
+import type { ReactorState } from "../types";
type Palette = {
primary: string;
@@ -9,14 +8,42 @@ type Palette = {
glow: string;
};
+/**
+ * State palettes, tuned to the same family as the UI tokens so the orb and the
+ * room around it read as one object. The mapping is semantic, matching the
+ * status-dot language the app already speaks:
+ * cyan = Iris herself (online, hearing you)
+ * amber = Iris's voice going out (the one warm state, so "she is talking"
+ * is unmistakable against the cool resting states)
+ * violet = Hermes is working — the same violet as the Hermes status dot
+ * slate = dormant
+ */
const PALETTES: Record
= {
- idle: { primary: "120, 170, 150", secondary: "150, 185, 165", accent: "210, 225, 218", glow: "150, 205, 180" },
- online: { primary: "18, 163, 148", secondary: "70, 200, 175", accent: "230, 255, 248", glow: "60, 195, 170" },
- listening: { primary: "40, 205, 170", secondary: "18, 163, 148", accent: "236, 255, 250", glow: "70, 214, 185" },
- speaking: { primary: "238, 122, 92", secondary: "255, 188, 108", accent: "255, 250, 230", glow: "255, 154, 104" },
- working: { primary: "120, 180, 120", secondary: "40, 200, 170", accent: "252, 255, 230", glow: "130, 195, 150" },
+ idle: { primary: "130, 158, 180", secondary: "150, 175, 195", accent: "215, 228, 240", glow: "150, 180, 205" },
+ online: { primary: "34, 190, 205", secondary: "80, 216, 226", accent: "230, 253, 255", glow: "60, 200, 215" },
+ listening: { primary: "56, 214, 236", secondary: "34, 190, 205", accent: "236, 253, 255", glow: "80, 220, 240" },
+ speaking: { primary: "245, 160, 92", secondary: "255, 200, 120", accent: "255, 248, 232", glow: "252, 176, 104" },
+ working: { primary: "150, 130, 245", secondary: "186, 160, 255", accent: "244, 240, 255", glow: "160, 140, 250" },
+};
+
+/**
+ * The room's light color per state. Published as `--orb-accent` on the deck /
+ * HUD root so the ambient glow, glass edge-light and hairlines are all tinted
+ * by whatever Iris is doing. Single source of truth — the orb chrome, the deck
+ * and the HUD all read from here.
+ */
+export const ORB_ACCENT: Record = {
+ idle: PALETTES.idle.primary,
+ online: PALETTES.online.primary,
+ listening: PALETTES.listening.primary,
+ speaking: PALETTES.speaking.primary,
+ working: PALETTES.working.primary,
};
+/** Energy of an idle/asleep reactor. Anything soft and glowing must be keyed
+ * off the distance above this, so a dark orb throws no light at all. */
+const REST_ENERGY = 0.18;
+
function drawArc(
c: CanvasRenderingContext2D,
x: number,
@@ -130,7 +157,7 @@ export default function ReactorCore({
if (s === "working") return 0.88;
if (s === "listening") return 0.72;
if (s === "online") return 0.45;
- return 0.18;
+ return REST_ENERGY;
}
function draw(time: number) {
@@ -160,15 +187,24 @@ export default function ReactorCore({
c.clearRect(0, 0, width, height);
- // Soft reactor halo
- const halo = c.createRadialGradient(cx, cy, 0, cx, cy, base * 0.95);
- halo.addColorStop(0, `rgba(${palette.glow}, ${0.32 + energy * 0.24})`);
- halo.addColorStop(0.34, `rgba(${palette.glow}, ${0.12 + energy * 0.08})`);
- halo.addColorStop(1, "rgba(0,0,0,0)");
- c.fillStyle = halo;
- c.beginPath();
- c.arc(cx, cy, base * 0.95, 0, Math.PI * 2);
- c.fill();
+ // Soft reactor halo — light thrown by a running reactor, so it must not
+ // exist when the reactor is off. It used to carry a fixed alpha floor,
+ // which left a wide glow disc (bigger than the drawn ring) sitting behind
+ // a sleeping orb; desaturated by the asleep filter it read as a grey
+ // shadow blob rather than as light. The gate reaches full strength by the
+ // time Iris is merely online, so the awake look is unchanged.
+ const bloom = Math.min(1, Math.max(0, (energy - REST_ENERGY) / 0.27));
+ if (bloom > 0.01) {
+ const haloR = base * 0.95;
+ const halo = c.createRadialGradient(cx, cy, 0, cx, cy, haloR);
+ halo.addColorStop(0, `rgba(${palette.glow}, ${bloom * (0.34 + energy * 0.22)})`);
+ halo.addColorStop(0.34, `rgba(${palette.glow}, ${bloom * (0.12 + energy * 0.08)})`);
+ halo.addColorStop(1, "rgba(0,0,0,0)");
+ c.fillStyle = halo;
+ c.beginPath();
+ c.arc(cx, cy, haloR, 0, Math.PI * 2);
+ c.fill();
+ }
// Outer micro ticks (futuristic HUD radial scale)
const tickCount = 144;
diff --git a/src/components/WorkCard.tsx b/src/components/WorkCard.tsx
index cb94baf..f79cdcb 100644
--- a/src/components/WorkCard.tsx
+++ b/src/components/WorkCard.tsx
@@ -82,6 +82,10 @@ export default function WorkCard({
className={`wcard ${active ? "working" : ""} ${expandable ? "expandable" : ""} ${
accepted ? "accepted" : ""
}`}
+ /* Drives the status-colored rail on the card's left edge. An attribute
+ rather than a class so Hermes status strings can never collide with
+ layout class names. */
+ data-status={status}
data-task-id={expandable ? task.id : undefined}
onPointerEnter={onFocus}
onFocus={onFocus}
diff --git a/src/components/WorkStream.tsx b/src/components/WorkStream.tsx
index 4182f2b..f9a278c 100644
--- a/src/components/WorkStream.tsx
+++ b/src/components/WorkStream.tsx
@@ -69,7 +69,11 @@ export default function WorkStream({
{tasks.length === 0 ? (
-
No Hermes runs yet. Ask Iris to take on a task.
+
+
+
+
No Hermes runs yet
+
Ask Iris to take on a task and it will stream in here.
{testDataEnabled ? (
Load demo tasks
diff --git a/src/styles/base.css b/src/styles/base.css
index 29030ed..46adca1 100644
--- a/src/styles/base.css
+++ b/src/styles/base.css
@@ -17,7 +17,7 @@ body {
overflow: hidden;
color: var(--text);
font-family: var(--font-ui);
- font-size: 14px;
+ font-size: var(--t-base);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
background: transparent;
@@ -27,8 +27,8 @@ body {
display: grid;
place-content: center;
min-height: 100vh;
- gap: 12px;
- padding: 32px;
+ gap: var(--s-3);
+ padding: var(--s-7);
text-align: center;
color: var(--text);
background: var(--bg);
@@ -41,7 +41,7 @@ body {
.fatal-boundary button {
justify-self: center;
- padding: 9px 14px;
+ padding: var(--s-2) var(--s-4);
border-radius: var(--r-sm);
color: var(--cyan-bright);
background: rgba(var(--cyan-rgb), 0.1);
@@ -72,55 +72,20 @@ input:focus-visible {
outline-offset: 2px;
}
-/* ===== Ambient layers (decorative, pointer-transparent) ===== */
+/* ===== No ambient layer =====
+ The deck background is a single flat fill, and that is the design, not a
+ placeholder. It has held, in order: an aurora of six blurred colour washes on
+ two drifting plates plus a full-window vignette (read as unrelated purple and
+ cyan smudges, and caused all the banding), and then one 340px glow behind the
+ reactor. That glow had to go too: once the background is flat, the only
+ brightness variation left IS the glow, so a bright centre falling off to dark
+ corners is precisely what vignetting looks like. Any single soft light on a
+ flat dark field will do the same thing.
-/* Faint drifting nebula: cyan + violet depth, kept very low so panels read clean.
- Absolute (not fixed) so the deck's rounded corners clip them. */
-.hud-nebula {
- position: absolute;
- inset: -22%;
- z-index: 0;
- pointer-events: none;
- background:
- radial-gradient(42% 42% at 24% 22%, rgba(var(--cyan-rgb), 0.07), transparent 70%),
- radial-gradient(40% 44% at 80% 18%, rgba(var(--violet-rgb), 0.07), transparent 72%),
- radial-gradient(50% 50% at 62% 88%, rgba(56, 89, 179, 0.09), transparent 76%);
- filter: blur(60px);
- animation: nebula-drift 40s ease-in-out infinite alternate;
-}
-
-/* Halo behind the orb so the core feels like the light source of the room */
-.hud-glow {
- position: absolute;
- inset: 0;
- z-index: 0;
- pointer-events: none;
- background: radial-gradient(
- 36% 44% at 50% 42%,
- rgba(var(--cyan-rgb), 0.075),
- rgba(var(--cyan-rgb), 0.02) 55%,
- transparent 75%
- );
- transition: opacity 1.2s ease;
-}
-
-.deck.asleep .hud-glow {
- opacity: 0.35;
-}
-
-.hud-vignette {
- position: absolute;
- inset: 0;
- z-index: 0;
- pointer-events: none;
- box-shadow: inset 0 0 220px rgba(0, 0, 0, 0.72);
-}
-
-@keyframes nebula-drift {
- 0% { transform: translate3d(-1.5%, -1%, 0) scale(1); }
- 50% { transform: translate3d(2%, 1.5%, 0) scale(1.06); }
- 100% { transform: translate3d(-1%, 2%, 0) scale(1.02); }
-}
+ The reactor canvas draws its own light, which is the focal point the room
+ needs. Two rules to keep this calm: gradients on dark surfaces must be small
+ and steep, and depth comes from a step of fill tone plus spacing rather than
+ from washes or outlines. */
/* ===== Shared scrollbars ===== */
::-webkit-scrollbar {
@@ -129,7 +94,7 @@ input:focus-visible {
}
::-webkit-scrollbar-thumb {
- border-radius: 999px;
+ border-radius: var(--r-pill);
background: rgba(148, 196, 255, 0.14);
}
@@ -141,57 +106,63 @@ input:focus-visible {
background: transparent;
}
-/* ===== Shared micro-label (panel headers etc.) ===== */
+/* ===== Panel header =====
+ Was 10px tracked-out uppercase mono, same as every other label in the app.
+ Now it speaks in the UI font at a legible size: the instrument voice is
+ rationed to real readouts (telemetry, run ids, boot log) so that when
+ something IS mono-tracked, it means something. */
.col-head {
display: flex;
align-items: center;
- gap: 8px;
- padding: 15px 16px 11px;
- font-family: var(--font-mono);
- font-size: 10px;
- font-weight: 500;
- letter-spacing: 0.24em;
- text-transform: uppercase;
- color: var(--muted);
+ gap: var(--s-2);
+ padding: var(--s-4) var(--s-4) var(--s-3);
+ font-size: var(--t-sm);
+ font-weight: var(--w-semi);
+ letter-spacing: var(--tr-snug);
+ color: var(--text-soft);
}
.col-head svg {
- color: rgba(var(--cyan-rgb), 0.66);
+ color: rgba(var(--accent-rgb), 0.8);
+ transition: color var(--accent-ease);
}
.col-head .count {
display: inline-grid;
place-items: center;
- min-width: 19px;
- height: 19px;
- padding: 0 6px;
- border-radius: 999px;
- font-size: 10px;
- letter-spacing: 0.05em;
+ min-width: 20px;
+ height: 20px;
+ padding: 0 var(--s-2);
+ border-radius: var(--r-pill);
+ font-family: var(--font-mono);
+ font-size: var(--t-2xs);
+ font-weight: var(--w-medium);
+ letter-spacing: 0;
color: var(--cyan-bright);
- background: rgba(var(--cyan-rgb), 0.1);
- box-shadow: inset 0 0 0 1px rgba(var(--cyan-rgb), 0.18);
+ background: rgba(var(--cyan-rgb), 0.12);
+ box-shadow: inset 0 0 0 1px rgba(var(--cyan-rgb), 0.2);
}
.col-head .head-state {
margin-left: auto;
- font-size: 9px;
- letter-spacing: 0.2em;
+ font-family: var(--font-mono);
+ font-size: var(--t-2xs);
+ font-weight: var(--w-medium);
+ letter-spacing: var(--tr-caps);
+ text-transform: uppercase;
color: var(--muted);
- opacity: 0.7;
}
.col-head .view-all {
display: inline-flex;
align-items: center;
- gap: 3px;
- margin-left: 8px;
- font-family: var(--font-mono);
- font-size: 9.5px;
- letter-spacing: 0.14em;
- text-transform: uppercase;
+ gap: var(--s-1);
+ margin-left: var(--s-2);
+ font-size: var(--t-xs);
+ font-weight: var(--w-medium);
+ letter-spacing: 0;
color: var(--text-soft);
- transition: color 0.15s ease;
+ transition: color var(--dur-fast) ease;
}
.col-head .count + .view-all,
@@ -200,55 +171,85 @@ input:focus-visible {
}
.col-head .view-all + .view-all {
- margin-left: 10px;
+ margin-left: var(--s-3);
}
.col-head .view-all:hover {
color: var(--cyan-bright);
}
-/* ===== Shared empty state ===== */
+/* ===== Shared empty state =====
+ The first thing a new user sees in both columns, so it gets a real
+ structure: a soft accent-ringed icon, a lead line, then a quieter hint. */
.empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: var(--s-3);
margin: auto;
- padding: 30px 18px;
+ padding: var(--s-7) var(--s-5);
text-align: center;
- font-size: 12.5px;
- line-height: 1.55;
color: var(--muted);
}
+.empty-icon {
+ display: grid;
+ place-items: center;
+ width: 46px;
+ height: 46px;
+ border-radius: var(--r-pill);
+ color: rgba(var(--accent-rgb), 0.85);
+ background: radial-gradient(circle, rgba(var(--accent-rgb), 0.14), transparent 72%);
+ box-shadow: inset 0 0 0 1px rgba(var(--accent-rgb), 0.2);
+ transition: color var(--accent-ease), box-shadow var(--accent-ease);
+}
+
.empty p {
- margin: 0 0 12px;
+ margin: 0;
+ max-width: 25ch;
+ font-size: var(--t-md);
+ font-weight: var(--w-medium);
+ line-height: var(--lh-snug);
+ color: var(--text-soft);
+}
+
+.empty small {
+ display: block;
+ max-width: 28ch;
+ font-size: var(--t-sm);
+ line-height: var(--lh-body);
+ color: var(--muted);
}
.demo-load {
- padding: 7px 13px;
- border-radius: 999px;
+ padding: var(--s-2) var(--s-3);
+ border-radius: var(--r-pill);
color: var(--cyan-bright);
- background: rgba(var(--cyan-rgb), 0.07);
- box-shadow: inset 0 0 0 1px rgba(var(--cyan-rgb), 0.2);
- font-family: var(--font-mono);
- font-size: 9.5px;
- font-weight: 500;
- letter-spacing: 0.16em;
- text-transform: uppercase;
- transition: background 0.15s ease, box-shadow 0.15s ease;
+ background: rgba(var(--cyan-rgb), 0.08);
+ box-shadow: inset 0 0 0 1px rgba(var(--cyan-rgb), 0.22);
+ font-size: var(--t-xs);
+ font-weight: var(--w-medium);
+ letter-spacing: 0;
+ transition: background var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}
.demo-load:hover {
- background: rgba(var(--cyan-rgb), 0.13);
- box-shadow: inset 0 0 0 1px rgba(var(--cyan-rgb), 0.34);
+ background: rgba(var(--cyan-rgb), 0.15);
+ box-shadow: inset 0 0 0 1px rgba(var(--cyan-rgb), 0.36);
}
-/* ===== Shared status badge (dot + mono uppercase, no filled pill) ===== */
+/* ===== Shared status badge (dot + mono uppercase, no filled pill) =====
+ Status IS a genuine readout, so this keeps the instrument voice — but at a
+ legible 10px floor instead of 9.5px. */
.badge {
display: inline-flex;
align-items: center;
- gap: 6px;
+ gap: var(--s-2);
font-family: var(--font-mono);
- font-size: 9.5px;
- font-weight: 600;
- letter-spacing: 0.2em;
+ font-size: var(--t-2xs);
+ font-weight: var(--w-semi);
+ letter-spacing: var(--tr-wide);
text-transform: uppercase;
color: var(--text-soft);
}
@@ -284,7 +285,6 @@ input:focus-visible {
}
@media (prefers-reduced-motion: reduce) {
- .hud-nebula,
.badge.working::before,
.badge.started::before,
.badge.running::before {
diff --git a/src/styles/brain.css b/src/styles/brain.css
index 628bd08..50a24c5 100644
--- a/src/styles/brain.css
+++ b/src/styles/brain.css
@@ -20,12 +20,9 @@
position: absolute;
inset: 0;
pointer-events: none;
- background: radial-gradient(
- ellipse 90% 80% at 50% 46%,
- rgba(2, 5, 10, 0.16) 0%,
- rgba(2, 5, 10, 0.24) 55%,
- rgba(2, 5, 10, 0.38) 100%
- );
+ /* Flat scrim, not a full-size radial: same reason as the deck background — a
+ soft dark wash this large has too few 8-bit levels to cross and bands. */
+ background: rgba(6, 10, 18, 0.26);
animation: brain-in 0.5s cubic-bezier(0.2, 0.8, 0.2, 1);
}
@@ -67,26 +64,25 @@
pointer-events: none;
}
-/* Title pill, top-left */
+/* Title pill, top-left — same chip material as the HUD's Tasks/Comms toggles */
.brain-title {
position: absolute;
top: 18px;
left: 22px;
display: inline-flex;
align-items: center;
- gap: 8px;
- padding: 7px 14px;
- border-radius: 999px;
- font-family: var(--font-mono);
- font-size: 9.5px;
- font-weight: 500;
- letter-spacing: 0.2em;
- text-transform: uppercase;
- color: #dff6ff;
- background: rgba(22, 30, 47, 0.7);
+ gap: var(--s-2);
+ padding: var(--s-1) var(--s-3);
+ border-radius: var(--r-pill);
+ font-size: var(--t-xs);
+ font-weight: var(--w-semi);
+ letter-spacing: var(--tr-snug);
+ color: var(--hud-text);
+ text-shadow: var(--hud-shadow-text-soft);
+ background: var(--hud-chip);
box-shadow:
- inset 0 1px 0 rgba(255, 255, 255, 0.14),
- inset 0 0 0 1px rgba(255, 255, 255, 0.16);
+ var(--hud-inner),
+ inset 0 0 0 1px var(--hud-edge);
}
.brain-title svg {
@@ -94,8 +90,10 @@
}
.brain-count {
+ font-family: var(--font-mono);
+ font-size: var(--t-2xs);
color: var(--cyan-bright);
- letter-spacing: 0.08em;
+ letter-spacing: 0;
}
/* Top-center — the top-right corner belongs to the HUD Tasks chip */
@@ -117,24 +115,24 @@
justify-content: center;
gap: 6px 12px;
max-width: 56vw;
- padding: 7px 16px;
- border-radius: 999px;
- background: rgba(22, 30, 47, 0.55);
+ padding: var(--s-2) var(--s-4);
+ border-radius: var(--r-pill);
+ background: var(--hud-chip);
box-shadow:
- inset 0 1px 0 rgba(255, 255, 255, 0.1),
- inset 0 0 0 1px rgba(255, 255, 255, 0.12);
+ var(--hud-inner),
+ inset 0 0 0 1px var(--hud-edge);
}
.brain-legend-item {
display: inline-flex;
align-items: center;
- gap: 6px;
+ gap: var(--s-2);
font-family: var(--font-mono);
- font-size: 9px;
- letter-spacing: 0.12em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-label);
text-transform: uppercase;
- color: #c3cfe0;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
+ color: var(--hud-text-soft);
+ text-shadow: var(--hud-shadow-text-soft);
}
.brain-legend-item i {
@@ -149,15 +147,16 @@
left: 50%;
top: 46%;
transform: translate(-50%, -50%);
- padding: 10px 18px;
- border-radius: 999px;
- font-size: 13px;
- color: #f2f7ff;
- text-shadow: 0 1px 3px rgba(0, 0, 0, 0.65);
- background: rgba(22, 30, 47, 0.72);
+ padding: var(--s-3) var(--s-5);
+ border-radius: var(--r-pill);
+ font-size: var(--t-md);
+ font-weight: var(--w-medium);
+ color: var(--hud-text);
+ text-shadow: var(--hud-shadow-text);
+ background: var(--hud-fill-strong);
box-shadow:
- inset 0 1px 0 rgba(255, 255, 255, 0.14),
- inset 0 0 0 1px rgba(255, 255, 255, 0.16);
+ var(--hud-inner),
+ inset 0 0 0 1px var(--hud-edge);
animation: brain-pulse 1.8s ease-in-out infinite;
}
@@ -171,7 +170,7 @@
top: auto;
bottom: 104px;
transform: translateX(-50%);
- font-size: 12px;
+ font-size: var(--t-sm);
animation: brain-toast 2.6s ease forwards;
}
@@ -180,7 +179,7 @@
top: auto;
bottom: 60px;
transform: translateX(-50%);
- font-size: 12px;
+ font-size: var(--t-sm);
color: #d9e6f5;
animation: none;
white-space: nowrap;
@@ -188,7 +187,7 @@
.brain-pill.filter strong {
color: var(--cyan-bright);
- font-weight: 600;
+ font-weight: var(--w-semi);
}
@keyframes brain-toast {
@@ -212,11 +211,11 @@
max-height: 78vh;
display: flex;
flex-direction: column;
- border-radius: 18px;
- background: linear-gradient(180deg, rgba(26, 36, 57, 0.8), rgba(10, 16, 28, 0.84));
- border: 1px solid rgba(255, 255, 255, 0.18);
+ border-radius: var(--r-lg);
+ background: var(--hud-fill-strong);
+ border: 1px solid var(--hud-edge);
box-shadow:
- inset 0 1px 0 rgba(255, 255, 255, 0.14),
+ var(--hud-inner),
0 30px 90px rgba(0, 0, 0, 0.5);
animation: brain-note-in 0.32s cubic-bezier(0.2, 0.8, 0.2, 1);
overflow: hidden;
@@ -242,21 +241,21 @@
.brain-note-folder {
font-family: var(--font-mono);
- font-size: 9px;
- font-weight: 600;
- letter-spacing: 0.22em;
+ font-size: var(--t-2xs);
+ font-weight: var(--w-semi);
+ letter-spacing: var(--tr-wide);
text-transform: uppercase;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
+ text-shadow: var(--hud-shadow-text-soft);
}
.brain-note-title {
- margin: 6px 16px 10px;
- font-size: 17px;
- font-weight: 600;
- letter-spacing: -0.01em;
- line-height: 1.3;
- color: #f2f7ff;
- text-shadow: 0 1px 3px rgba(0, 0, 0, 0.65);
+ margin: var(--s-2) var(--s-4) var(--s-3);
+ font-size: var(--t-lg);
+ font-weight: var(--w-semi);
+ letter-spacing: var(--tr-tight);
+ line-height: var(--lh-tight);
+ color: var(--hud-text);
+ text-shadow: var(--hud-shadow-text);
}
.brain-note-meta {
@@ -269,10 +268,10 @@
.brain-meta-chip {
display: inline-flex;
align-items: center;
- gap: 6px;
- padding: 3px 9px;
- border-radius: 999px;
- font-size: 10.5px;
+ gap: var(--s-2);
+ padding: var(--s-05) var(--s-2);
+ border-radius: var(--r-pill);
+ font-size: var(--t-xs);
color: #e2ebf7;
background: rgba(255, 255, 255, 0.06);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
@@ -281,8 +280,8 @@
.brain-meta-chip em {
font-style: normal;
font-family: var(--font-mono);
- font-size: 8.5px;
- letter-spacing: 0.12em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-label);
text-transform: uppercase;
color: #9fb2c9;
}
@@ -296,9 +295,9 @@
}
.brain-note-body .markdown-body {
- font-size: 13.5px;
+ font-size: var(--t-base);
color: #d3ddec;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
+ text-shadow: var(--hud-shadow-text-soft);
}
.brain-note-body .markdown-body img {
@@ -328,8 +327,8 @@
.brain-note-loading,
.brain-note-error {
- margin: 8px 0;
- font-size: 12px;
+ margin: var(--s-2) 0;
+ font-size: var(--t-sm);
color: var(--muted);
}
@@ -345,10 +344,10 @@
.brain-links-label {
display: block;
- margin-bottom: 8px;
+ margin-bottom: var(--s-2);
font-family: var(--font-mono);
- font-size: 8.5px;
- letter-spacing: 0.2em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-wide);
text-transform: uppercase;
color: #9fb2c9;
}
@@ -363,9 +362,9 @@
}
.brain-link-chip {
- padding: 4px 10px;
- border-radius: 999px;
- font-size: 11px;
+ padding: var(--s-1) var(--s-3);
+ border-radius: var(--r-pill);
+ font-size: var(--t-xs);
color: var(--cyan-bright);
background: rgba(var(--cyan-rgb), 0.08);
box-shadow: inset 0 0 0 1px rgba(var(--cyan-rgb), 0.22);
diff --git a/src/styles/deck.css b/src/styles/deck.css
index a07b32d..74b966f 100644
--- a/src/styles/deck.css
+++ b/src/styles/deck.css
@@ -1,5 +1,9 @@
/* =====================================================================
Orbital Deck layout — LEFT = You · CENTER = Iris · RIGHT = Work
+
+ Hierarchy rule: the orb is the subject. The side columns are RECESSED
+ rails that frame it, not cards that compete with it. Everything
+ atmospheric is tinted by `--accent-rgb` (the live reactor state).
===================================================================== */
.deck {
@@ -7,31 +11,40 @@
display: flex;
flex-direction: column;
height: 100vh;
- padding: 12px 18px 12px;
- gap: 12px;
+ padding: var(--s-3) var(--s-5);
+ gap: var(--s-3);
/* The window is transparent; the deck owns its background + rounded frame. */
- border-radius: 14px;
+ border-radius: var(--r-md);
overflow: hidden;
- background:
- radial-gradient(85% 70% at 50% 30%, rgba(17, 34, 64, 0.5), transparent 70%),
- linear-gradient(180deg, var(--bg-1) 0%, var(--bg-0) 100%);
- box-shadow: inset 0 0 0 1px rgba(148, 196, 255, 0.06);
-}
-
-.deck > *:not(.hud-nebula):not(.hud-glow):not(.hud-vignette) {
+ /* Flat. Deliberately. This carried two window-sized radial washes (88% and
+ 92% wide) plus a --bg-1 -> --bg-0 ramp; all three spanned fewer 8-bit
+ levels than the pixels they covered, which is the definition of a gradient
+ that must band. The rule: if a gradient crosses more pixels than it has
+ code levels to cross them with, it is not a gradient, it is a stack of
+ plateaus with visible edges. The reactor canvas is the room's light; the
+ background stays flat. */
+ background: var(--bg-1);
+ box-shadow: inset 0 0 0 1px rgba(148, 196, 255, 0.07);
+ transition: background var(--accent-ease);
+}
+
+.deck > * {
position: relative;
z-index: 1;
}
-/* ===== Glass panels ===== */
+/* ===== Surfaces =====
+ Containment comes from FILL, not from outlines. Every surface here used to
+ carry its own 1px border, and they nest three deep — panel, then message
+ bubble, then card — so the deck read as a cage of boxes inside boxes. A
+ surface that is a step lighter than its parent already reads as separate;
+ adding a line around it as well is saying the same thing twice, and the
+ lines are what you notice. Spacing and one step of tone do the work now. */
.deck-panel {
- border-radius: var(--r-panel);
- background: var(--panel);
- border: 1px solid var(--hairline);
- box-shadow:
- inset 0 1px 0 var(--edge-light),
- var(--shadow-panel);
+ border-radius: var(--r-lg);
+ background: var(--panel-recessed);
backdrop-filter: blur(26px) saturate(1.25);
+ transition: background var(--accent-ease);
}
/* ===== Top bar ===== */
@@ -40,15 +53,14 @@
grid-template-columns: 1fr auto 1fr;
align-items: center;
min-height: 42px;
- padding: 4px 6px 8px;
- border-bottom: 1px solid var(--hairline);
+ padding: var(--s-1) var(--s-2) var(--s-2);
-webkit-app-region: drag;
}
.deck-top-left {
display: flex;
align-items: center;
- gap: 16px;
+ gap: var(--s-4);
/* The REAL macOS traffic lights (titleBarStyle: hiddenInset) occupy
window x 22-74; clear their footprint before the status dots start. */
padding-left: 66px;
@@ -58,7 +70,7 @@
display: flex;
align-items: center;
justify-content: flex-end;
- gap: 9px;
+ gap: var(--s-2);
}
/* Only interactive controls opt out of the drag region. */
@@ -74,16 +86,17 @@
.brand-mark {
font-family: var(--font-display);
- font-weight: 600;
- font-size: 17px;
- letter-spacing: 0.52em;
- margin-right: -0.52em; /* optically recenter the tracked-out wordmark */
+ font-weight: var(--w-semi);
+ font-size: var(--t-lg);
+ letter-spacing: var(--tr-brand);
+ margin-right: calc(var(--tr-brand) * -1); /* optically recenter the tracked wordmark */
color: #eaf6ff;
- text-shadow: 0 0 22px rgba(var(--cyan-rgb), 0.4);
+ text-shadow: 0 0 22px rgba(var(--accent-rgb), 0.45);
+ transition: text-shadow var(--accent-ease);
user-select: none;
}
-/* Ghost icon buttons (settings / hand) */
+/* Ghost icon buttons (settings / hand / HUD) */
.theme-toggle {
display: grid;
place-items: center;
@@ -93,12 +106,17 @@
color: var(--text-soft);
background: rgba(255, 255, 255, 0.03);
box-shadow: inset 0 0 0 1px var(--hairline);
- transition: color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, transform 0.18s ease;
+ transition:
+ color var(--dur-mid) ease,
+ box-shadow var(--dur-mid) ease,
+ background var(--dur-mid) ease,
+ transform var(--dur-fast) ease;
}
.theme-toggle:hover {
transform: translateY(-1px);
color: var(--cyan-bright);
+ background: rgba(255, 255, 255, 0.055);
box-shadow: inset 0 0 0 1px var(--hairline-strong);
}
@@ -119,7 +137,7 @@
color: var(--muted);
background: rgba(255, 255, 255, 0.03);
box-shadow: inset 0 0 0 1px var(--hairline);
- transition: color 0.3s ease, box-shadow 0.3s ease;
+ transition: color var(--dur-slow) ease, box-shadow var(--dur-slow) ease;
}
.link-indicator.on {
@@ -130,23 +148,36 @@
animation: pulse-dot 2.6s ease-in-out infinite;
}
-/* ===== Status cluster ===== */
+/* ===== Status cluster =====
+ Was three identical free-floating mono labels. Now one seated instrument
+ cluster with hairline dividers, so it reads as a single system readout. */
.deck-status {
display: flex;
align-items: center;
- gap: 16px;
+ gap: var(--s-3);
+ padding: var(--s-1) var(--s-3);
+ border-radius: var(--r-pill);
+ background: rgba(255, 255, 255, 0.022);
+ box-shadow: inset 0 0 0 1px var(--hairline);
}
.status-dot {
display: inline-flex;
align-items: center;
- gap: 7px;
+ gap: var(--s-2);
font-family: var(--font-mono);
- font-size: 9.5px;
- font-weight: 500;
- letter-spacing: 0.2em;
+ font-size: var(--t-2xs);
+ font-weight: var(--w-medium);
+ letter-spacing: var(--tr-caps);
text-transform: uppercase;
color: var(--muted);
+ transition: color var(--dur-slow) ease;
+}
+
+/* Hairline dividers between channels rather than raw whitespace. */
+.status-dot + .status-dot {
+ padding-left: var(--s-3);
+ border-left: 1px solid var(--hairline);
}
.status-dot i {
@@ -154,7 +185,13 @@
height: 7px;
border-radius: 50%;
background: #273349;
- transition: background 0.3s ease, box-shadow 0.3s ease;
+ transition: background var(--dur-slow) ease, box-shadow var(--dur-slow) ease;
+}
+
+/* A live channel brightens its own label, not just its dot. */
+.status-dot.on,
+.status-dot.speaking {
+ color: var(--text-soft);
}
.status-dot.err i {
@@ -181,8 +218,8 @@
}
.status-dot.audio.speaking i {
- background: var(--coral);
- box-shadow: 0 0 12px rgba(var(--coral-rgb), 0.75);
+ background: var(--amber);
+ box-shadow: 0 0 12px rgba(var(--amber-rgb), 0.75);
animation: pulse-dot 1.1s ease-in-out infinite;
}
@@ -201,14 +238,14 @@
min-height: 0;
display: grid;
grid-template-columns: minmax(280px, 336px) minmax(0, 1fr) minmax(300px, 356px);
- gap: 12px;
+ gap: var(--s-3);
}
/* ===== Left column: You ===== */
.deck-left {
display: flex;
flex-direction: column;
- gap: 12px;
+ gap: var(--s-3);
min-height: 0;
}
@@ -226,49 +263,50 @@
overflow-y: auto;
display: flex;
flex-direction: column;
- gap: 9px;
- padding: 2px 14px 14px;
+ gap: var(--s-2);
+ padding: var(--s-05) var(--s-4) var(--s-4);
}
.bubble {
flex: 0 0 auto;
max-width: 88%;
- padding: 9px 13px;
- border-radius: 14px;
- font-size: 13px;
- line-height: 1.55;
+ padding: var(--s-2) var(--s-3);
+ border-radius: var(--r-md);
+ font-size: var(--t-md);
+ line-height: var(--lh-body);
color: var(--text);
white-space: pre-wrap;
word-break: break-word;
- animation: bubble-in 0.28s ease;
+ animation: bubble-in var(--dur-mid) var(--ease-out);
}
.bubble .who {
display: block;
- margin-bottom: 4px;
+ margin-bottom: var(--s-1);
font-family: var(--font-mono);
- font-size: 8.5px;
- font-weight: 600;
- letter-spacing: 0.22em;
+ font-size: var(--t-2xs);
+ font-weight: var(--w-semi);
+ letter-spacing: var(--tr-caps);
text-transform: uppercase;
}
+/* Iris speaks in an accent-tinted fill and hugs the left edge. Flat, borderless:
+ the tint alone says who is speaking. */
.bubble.iris {
align-self: flex-start;
- background: rgba(var(--cyan-rgb), 0.07);
- border: 1px solid rgba(var(--cyan-rgb), 0.13);
- border-bottom-left-radius: 5px;
+ background: rgba(var(--cyan-rgb), 0.1);
+ border-bottom-left-radius: var(--s-1);
}
.bubble.iris .who {
- color: rgba(var(--cyan-rgb), 0.8);
+ color: rgba(var(--cyan-rgb), 0.85);
}
+/* You speak in a neutral fill on the right. */
.bubble.self {
align-self: flex-end;
- background: rgba(255, 255, 255, 0.035);
- border: 1px solid rgba(255, 255, 255, 0.07);
- border-bottom-right-radius: 5px;
+ background: rgba(255, 255, 255, 0.06);
+ border-bottom-right-radius: var(--s-1);
}
.bubble.self .who {
@@ -288,9 +326,9 @@
.camera-frame {
position: relative;
- margin: 0 12px 12px;
+ margin: 0 var(--s-3) var(--s-3);
aspect-ratio: 4 / 3;
- border-radius: var(--r-card);
+ border-radius: var(--r-md);
overflow: hidden;
background: #040910;
box-shadow: inset 0 0 0 1px var(--hairline);
@@ -307,8 +345,9 @@
position: absolute;
inset: 0;
pointer-events: none;
- background: linear-gradient(transparent 62%, rgba(var(--cyan-rgb), 0.07));
+ background: linear-gradient(transparent 62%, rgba(var(--accent-rgb), 0.09));
box-shadow: inset 0 0 44px rgba(3, 10, 18, 0.7);
+ transition: background var(--accent-ease);
}
.hand-skeleton {
@@ -350,20 +389,20 @@
.gesture-chip {
position: absolute;
- left: 10px;
- bottom: 10px;
+ left: var(--s-3);
+ bottom: var(--s-3);
display: inline-flex;
align-items: center;
- gap: 6px;
- padding: 5px 10px;
- border-radius: 999px;
+ gap: var(--s-2);
+ padding: var(--s-1) var(--s-3);
+ border-radius: var(--r-pill);
font-family: var(--font-mono);
- font-size: 9.5px;
- font-weight: 500;
- letter-spacing: 0.08em;
+ font-size: var(--t-2xs);
+ font-weight: var(--w-medium);
+ letter-spacing: var(--tr-label);
color: #dff6ff;
- background: rgba(4, 12, 20, 0.66);
- box-shadow: inset 0 0 0 1px rgba(148, 196, 255, 0.14);
+ background: rgba(4, 12, 20, 0.7);
+ box-shadow: inset 0 0 0 1px rgba(148, 196, 255, 0.16);
backdrop-filter: blur(8px);
}
@@ -375,7 +414,7 @@
/* Pinch is the one state you must never be unsure about — loud orange. */
.gesture-chip.pinch {
color: #ff9f43;
- font-weight: 700;
+ font-weight: var(--w-bold);
opacity: 1;
}
.gesture-chip.pinch .dot {
@@ -393,16 +432,20 @@
.cam-status {
position: absolute;
- right: 10px;
- top: 10px;
+ right: var(--s-3);
+ top: var(--s-3);
display: inline-flex;
align-items: center;
- gap: 6px;
+ gap: var(--s-2);
+ padding: var(--s-05) var(--s-2);
+ border-radius: var(--r-pill);
font-family: var(--font-mono);
- font-size: 8.5px;
- letter-spacing: 0.16em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-label);
color: #cfeeff;
text-transform: uppercase;
+ background: rgba(4, 12, 20, 0.5);
+ backdrop-filter: blur(6px);
}
.cam-status i {
@@ -416,24 +459,24 @@
.cam-error {
position: absolute;
- inset: auto 10px 42px;
+ inset: auto var(--s-3) 42px;
z-index: 3;
- padding: 7px 8px;
- border-radius: 7px;
+ padding: var(--s-2);
+ border-radius: var(--r-xs);
color: var(--coral);
- background: rgba(3, 5, 9, 0.82);
+ background: rgba(3, 5, 9, 0.85);
box-shadow: inset 0 0 0 1px rgba(var(--coral-rgb), 0.25);
- font-size: 9px;
- line-height: 1.4;
+ font-size: var(--t-2xs);
+ line-height: var(--lh-snug);
}
.camera-off {
- margin: 0 12px 12px;
- padding: 18px 14px;
- border-radius: var(--r-card);
+ margin: 0 var(--s-3) var(--s-3);
+ padding: var(--s-5) var(--s-4);
+ border-radius: var(--r-md);
text-align: center;
- font-size: 12px;
- line-height: 1.5;
+ font-size: var(--t-sm);
+ line-height: var(--lh-body);
color: var(--muted);
background: rgba(255, 255, 255, 0.02);
box-shadow: inset 0 0 0 1px var(--hairline);
@@ -446,13 +489,13 @@
flex-direction: column;
align-items: center;
justify-content: center;
- gap: 14px;
- padding: 0 0 22px;
+ gap: var(--s-4);
+ padding: 0 0 var(--s-6);
min-width: 0;
z-index: 2;
}
-/* --- Orb stage (preserved: same core, ring and radar chrome as before) --- */
+/* --- Orb stage (the core, ring and radar chrome are unchanged) --- */
.orb-stage {
width: min(44vh, 400px);
height: min(44vh, 400px);
@@ -470,6 +513,12 @@
height: 100%;
}
+/* There is deliberately no plinth under the orb. A blurred elliptical light pool
+ plus a specular floor line used to sit here to stop the orb floating in the
+ gap between the two panels, but a large soft ellipse under the reactor reads
+ as a drop shadow, not as a stage. The reactor is centered and much brighter
+ than the rails, which is enough to make it outrank them. */
+
/* Orb chrome: rotating tick ring + sweeping radar arc around the core */
.orb-ring,
.orb-radar {
@@ -484,7 +533,7 @@
inset: -3%;
background: repeating-conic-gradient(
from 0deg,
- rgba(var(--orb-accent, 63, 208, 176), 0.32) 0deg 0.7deg,
+ rgba(var(--orb-accent, 34, 190, 205), 0.32) 0deg 0.7deg,
transparent 0.7deg 6deg
);
-webkit-mask: radial-gradient(circle, transparent 72%, #000 73%, #000 77%, transparent 78%);
@@ -496,9 +545,9 @@
inset: 1%;
background: conic-gradient(
from 0deg,
- rgba(var(--orb-accent, 63, 208, 176), 0) 0deg,
- rgba(var(--orb-accent, 63, 208, 176), 0.22) 38deg,
- rgba(var(--orb-accent, 63, 208, 176), 0) 80deg
+ rgba(var(--orb-accent, 34, 190, 205), 0) 0deg,
+ rgba(var(--orb-accent, 34, 190, 205), 0.22) 38deg,
+ rgba(var(--orb-accent, 34, 190, 205), 0) 80deg
);
-webkit-mask: radial-gradient(circle, transparent 58%, #000 59%, #000 71%, transparent 72%);
mask: radial-gradient(circle, transparent 58%, #000 59%, #000 71%, transparent 72%);
@@ -526,16 +575,17 @@
.caption {
max-width: 82%;
text-align: center;
- font-size: clamp(18px, 2.3vw, 25px);
+ font-size: var(--t-caption);
font-weight: 450;
- letter-spacing: -0.01em;
- line-height: 1.35;
+ letter-spacing: var(--tr-tight);
+ line-height: var(--lh-tight);
color: var(--text);
min-height: 1.4em;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
+ text-wrap: balance;
}
.caption.dim {
@@ -546,10 +596,10 @@
display: inline-block;
width: 2px;
height: 0.95em;
- margin-left: 7px;
+ margin-left: var(--s-2);
vertical-align: -2px;
- background: var(--cyan);
- box-shadow: 0 0 8px var(--cyan);
+ background: rgba(var(--accent-rgb), 1);
+ box-shadow: 0 0 8px rgba(var(--accent-rgb), 0.9);
animation: caret-blink 1s steps(1) infinite;
}
@@ -558,38 +608,42 @@
50.01%, 100% { opacity: 0; }
}
-/* --- Telemetry readout --- */
+/* --- Telemetry readout ---
+ This IS a genuine instrument readout, so it keeps the tracked mono voice —
+ at the 10px legibility floor, with the labels dimmer than their values so
+ you can scan the numbers. */
.telemetry {
display: flex;
align-items: center;
- gap: 10px;
+ gap: var(--s-3);
font-family: var(--font-mono);
- font-size: 10px;
- letter-spacing: 0.16em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-caps);
text-transform: uppercase;
color: var(--text-soft);
- opacity: 0.5;
- transition: opacity 0.4s ease;
+ opacity: 0.55;
+ transition: opacity var(--dur-slow) ease;
}
-.telemetry.live { opacity: 0.85; }
+.telemetry.live { opacity: 0.9; }
.telemetry i {
font-style: normal;
- color: rgba(var(--cyan-rgb), 0.75);
- margin-right: 6px;
+ color: rgba(var(--accent-rgb), 0.8);
+ margin-right: var(--s-2);
+ transition: color var(--accent-ease);
}
.telemetry .sep {
- opacity: 0.25;
+ opacity: 0.22;
}
/* --- Transport controls --- */
.transport {
display: flex;
align-items: center;
- gap: 14px;
- padding-top: 2px;
+ gap: var(--s-4);
+ padding-top: var(--s-05);
}
/* ===== Quick device pickers (Zoom-style carets) ===== */
@@ -598,7 +652,7 @@
position: relative;
display: inline-flex;
align-items: center;
- gap: 4px;
+ gap: var(--s-1);
}
.device-pick {
@@ -617,7 +671,11 @@
box-shadow:
inset 0 0 0 1px var(--hairline-strong),
0 4px 12px rgba(0, 0, 0, 0.4);
- transition: color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease, background 0.15s ease;
+ transition:
+ color var(--dur-fast) ease,
+ box-shadow var(--dur-fast) ease,
+ transform var(--dur-fast) ease,
+ background var(--dur-fast) ease;
}
.device-pick-trigger:hover,
@@ -637,13 +695,13 @@
position: fixed;
z-index: 400;
width: 260px;
- padding: 7px;
- border-radius: 14px;
+ padding: var(--s-2);
+ border-radius: var(--r-md);
background: linear-gradient(180deg, rgba(16, 28, 48, 0.97), rgba(8, 14, 26, 0.97));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.07),
inset 0 0 0 1px rgba(148, 196, 255, 0.16),
- 0 24px 70px rgba(0, 0, 0, 0.65);
+ var(--e-3);
backdrop-filter: blur(20px) saturate(1.25);
max-height: 300px;
overflow-y: auto;
@@ -651,12 +709,12 @@
.device-pick-menu.down {
transform-origin: top center;
- animation: device-menu-in 0.16s ease;
+ animation: device-menu-in var(--dur-fast) ease;
}
.device-pick-menu.side {
transform-origin: bottom left;
- animation: device-menu-in 0.16s ease;
+ animation: device-menu-in var(--dur-fast) ease;
}
@keyframes device-menu-in {
@@ -673,27 +731,27 @@
.device-pick-head {
display: flex;
align-items: center;
- gap: 7px;
- padding: 7px 11px 8px;
- margin-bottom: 4px;
+ gap: var(--s-2);
+ padding: var(--s-2) var(--s-3) var(--s-2);
+ margin-bottom: var(--s-1);
border-bottom: 1px solid rgba(148, 196, 255, 0.1);
- font-size: 10px;
- font-weight: 600;
- letter-spacing: 0.16em;
+ font-size: var(--t-2xs);
+ font-weight: var(--w-semi);
+ letter-spacing: var(--tr-caps);
text-transform: uppercase;
- color: var(--cyan-soft, #7dd3fc);
+ color: var(--cyan-soft);
}
.device-pick-item {
display: flex;
align-items: center;
- gap: 9px;
+ gap: var(--s-2);
width: 100%;
- padding: 8px 11px;
- border-radius: 9px;
+ padding: var(--s-2) var(--s-3);
+ border-radius: var(--r-xs);
text-align: left;
- font-size: 12.5px;
- line-height: 1.3;
+ font-size: var(--t-md);
+ line-height: var(--lh-tight);
color: var(--text-soft);
transition: background 0.12s ease, color 0.12s ease;
}
@@ -728,8 +786,8 @@
}
.device-pick-empty {
- padding: 6px 11px 7px;
- font-size: 11px;
+ padding: var(--s-2) var(--s-3);
+ font-size: var(--t-xs);
color: var(--text-dim);
font-style: italic;
}
@@ -740,6 +798,9 @@
-webkit-app-region: no-drag;
}
+/* --- Transport buttons ---
+ Real hierarchy: the mic is the primary control and carries accent presence;
+ sleep reads as destructive BEFORE you hover it, not only on hover. */
.t-btn {
display: grid;
place-items: center;
@@ -747,7 +808,11 @@
color: var(--text-soft);
background: rgba(255, 255, 255, 0.03);
box-shadow: inset 0 0 0 1px var(--hairline-strong);
- transition: transform 0.16s ease, color 0.16s ease, box-shadow 0.2s ease, background 0.2s ease;
+ transition:
+ transform var(--dur-fast) ease,
+ color var(--dur-fast) ease,
+ box-shadow var(--dur-mid) ease,
+ background var(--dur-mid) ease;
}
.t-btn:hover {
@@ -759,20 +824,54 @@
}
.t-btn.small {
- width: 44px;
- height: 44px;
+ width: 46px;
+ height: 46px;
+}
+
+.t-btn.primary {
+ color: #eafcff;
+ background:
+ radial-gradient(circle at 50% 30%, rgba(var(--accent-rgb), 0.28), rgba(var(--accent-rgb), 0.1) 70%);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.16),
+ inset 0 0 0 1px rgba(var(--accent-rgb), 0.5),
+ 0 6px 24px -6px rgba(var(--accent-rgb), 0.5);
+}
+
+.t-btn.primary:hover {
+ color: #ffffff;
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.22),
+ inset 0 0 0 1px rgba(var(--accent-rgb), 0.75),
+ 0 10px 32px -6px rgba(var(--accent-rgb), 0.7);
}
.t-btn.muted {
color: var(--amber);
- box-shadow: inset 0 0 0 1px rgba(var(--amber-rgb), 0.4);
+ background: rgba(var(--amber-rgb), 0.1);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.12),
+ inset 0 0 0 1px rgba(var(--amber-rgb), 0.5);
+}
+
+.t-btn.muted:hover {
+ color: var(--amber);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.16),
+ inset 0 0 0 1px rgba(var(--amber-rgb), 0.7),
+ 0 8px 26px -8px rgba(var(--amber-rgb), 0.55);
+}
+
+.t-btn.danger {
+ color: rgba(248, 113, 113, 0.75);
+ box-shadow: inset 0 0 0 1px rgba(var(--coral-rgb), 0.24);
}
.t-btn.danger:hover {
color: var(--coral);
box-shadow:
- inset 0 0 0 1px rgba(var(--coral-rgb), 0.45),
- 0 8px 26px rgba(var(--coral-rgb), 0.14);
+ inset 0 0 0 1px rgba(var(--coral-rgb), 0.5),
+ 0 8px 26px -8px rgba(var(--coral-rgb), 0.4);
}
/* --- Asleep-state wake prompt --- */
@@ -780,17 +879,17 @@
display: flex;
flex-direction: column;
align-items: center;
- gap: 13px;
+ gap: var(--s-3);
}
.wake-say {
display: inline-flex;
align-items: center;
- gap: 9px;
- padding: 11px 22px;
- border-radius: 999px;
- font-size: clamp(15px, 1.8vw, 18px);
- font-weight: 500;
+ gap: var(--s-2);
+ padding: var(--s-3) var(--s-6);
+ border-radius: var(--r-pill);
+ font-size: var(--t-wake);
+ font-weight: var(--w-medium);
color: var(--text);
background: rgba(var(--cyan-rgb), 0.05);
box-shadow: inset 0 0 0 1px rgba(var(--cyan-rgb), 0.18);
@@ -799,7 +898,7 @@
.wake-say b {
color: var(--cyan-bright);
- font-weight: 600;
+ font-weight: var(--w-semi);
}
.wake-say svg {
@@ -814,11 +913,8 @@
.wake-keys {
display: inline-flex;
align-items: center;
- gap: 10px;
- font-family: var(--font-mono);
- font-size: 9.5px;
- letter-spacing: 0.18em;
- text-transform: uppercase;
+ gap: var(--s-3);
+ font-size: var(--t-sm);
color: var(--muted);
}
@@ -827,25 +923,28 @@
.wake-keys .combo {
display: inline-flex;
align-items: center;
- gap: 4px;
+ gap: var(--s-1);
}
.wake-keys .key {
display: inline-grid;
place-items: center;
- min-width: 21px;
- height: 21px;
- padding: 0 5px;
- border-radius: 6px;
- font-size: 11.5px;
+ min-width: 22px;
+ height: 22px;
+ padding: 0 var(--s-1);
+ border-radius: var(--r-xs);
+ font-family: var(--font-mono);
+ font-size: var(--t-xs);
letter-spacing: 0;
color: var(--cyan-bright);
background: rgba(var(--cyan-rgb), 0.09);
- box-shadow: inset 0 0 0 1px rgba(var(--cyan-rgb), 0.24);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.1),
+ inset 0 0 0 1px rgba(var(--cyan-rgb), 0.26);
}
.wake-sep {
- margin: 0 2px;
+ margin: 0 var(--s-05);
opacity: 0.4;
}
@@ -861,24 +960,25 @@
position: relative;
display: flex;
align-items: center;
- gap: 8px;
- padding: 0 12px 10px;
+ gap: var(--s-2);
+ padding: 0 var(--s-3) var(--s-3);
}
.session-chip {
display: inline-flex;
align-items: center;
- gap: 7px;
+ gap: var(--s-2);
min-width: 0;
- padding: 5px 11px;
- border-radius: 999px;
- font-family: var(--font-mono);
- font-size: 10px;
- letter-spacing: 0.08em;
+ padding: var(--s-1) var(--s-3);
+ border-radius: var(--r-pill);
+ font-size: var(--t-xs);
color: var(--text-soft);
background: rgba(var(--cyan-rgb), 0.05);
box-shadow: inset 0 0 0 1px var(--hairline-strong);
- transition: color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
+ transition:
+ color var(--dur-fast) ease,
+ box-shadow var(--dur-fast) ease,
+ background var(--dur-fast) ease;
}
.session-chip:hover {
@@ -907,15 +1007,15 @@
white-space: nowrap;
max-width: 190px;
font-family: var(--font-ui);
- font-size: 11px;
- font-weight: 500;
- letter-spacing: 0.01em;
+ font-size: var(--t-sm);
+ font-weight: var(--w-medium);
+ letter-spacing: 0;
}
.session-chip .chev {
flex: 0 0 auto;
color: var(--muted);
- transition: transform 0.18s ease;
+ transition: transform var(--dur-fast) ease;
}
.session-chip.open .chev {
@@ -933,7 +1033,10 @@
color: var(--text-soft);
background: rgba(255, 255, 255, 0.03);
box-shadow: inset 0 0 0 1px var(--hairline);
- transition: color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
+ transition:
+ color var(--dur-fast) ease,
+ box-shadow var(--dur-fast) ease,
+ transform var(--dur-fast) ease;
}
.session-new:hover {
@@ -944,25 +1047,25 @@
.session-menu {
position: absolute;
- left: 12px;
- right: 12px;
- top: calc(100% - 4px);
+ left: var(--s-3);
+ right: var(--s-3);
+ top: calc(100% - var(--s-1));
z-index: 30;
max-height: 300px;
overflow-y: auto;
- padding: 6px;
- border-radius: 12px;
+ padding: var(--s-2);
+ border-radius: var(--r-md);
background: #0a1322;
border: 1px solid var(--hairline-strong);
- box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6);
+ box-shadow: var(--e-3);
animation: ts-menu-in 0.14s ease;
}
.session-menu-head {
- padding: 6px 9px 7px;
+ padding: var(--s-2) var(--s-2) var(--s-2);
font-family: var(--font-mono);
- font-size: 8.5px;
- letter-spacing: 0.22em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-wide);
text-transform: uppercase;
color: var(--muted);
}
@@ -971,9 +1074,9 @@
width: 100%;
display: flex;
align-items: center;
- gap: 8px;
- padding: 8px 9px;
- border-radius: 8px;
+ gap: var(--s-2);
+ padding: var(--s-2);
+ border-radius: var(--r-xs);
text-align: left;
color: var(--text-soft);
transition: background 0.12s ease, color 0.12s ease;
@@ -997,13 +1100,13 @@
min-width: 0;
flex: 1;
display: grid;
- gap: 2px;
+ gap: var(--s-05);
}
.session-item-main strong {
- font-size: 12px;
- font-weight: 550;
- letter-spacing: -0.005em;
+ font-size: var(--t-sm);
+ font-weight: var(--w-medium);
+ letter-spacing: var(--tr-snug);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -1012,14 +1115,14 @@
.session-item-main em {
font-style: normal;
font-family: var(--font-mono);
- font-size: 9px;
+ font-size: var(--t-2xs);
color: var(--muted);
}
.session-empty {
- padding: 12px 10px;
+ padding: var(--s-3) var(--s-3);
text-align: center;
- font-size: 11.5px;
+ font-size: var(--t-sm);
color: var(--muted);
}
@@ -1029,46 +1132,76 @@
overflow-y: auto;
display: flex;
flex-direction: column;
- gap: 9px;
- padding: 2px 12px 12px;
+ gap: var(--s-2);
+ padding: var(--s-05) var(--s-3) var(--s-3);
}
+/* ===== Work cards =====
+ Every card now carries a status-colored rail on its left edge, so you can
+ read the state of the whole stream peripherally without reading a word.
+ (Previously only `working` had a rail, and it was always amber.) */
.wcard {
position: relative;
flex: 0 0 auto;
- padding: 13px 14px;
- border-radius: var(--r-card);
- background: rgba(255, 255, 255, 0.028);
- border: 1px solid var(--hairline);
+ padding: var(--s-3) var(--s-4);
+ border-radius: var(--r-md);
+ background: rgba(255, 255, 255, 0.04);
cursor: default;
- transition: transform 0.16s ease, border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
+ transition:
+ transform var(--dur-fast) ease,
+ border-color var(--dur-mid) ease,
+ box-shadow var(--dur-mid) ease,
+ background var(--dur-mid) ease;
overflow: hidden;
}
+/* The status rail. */
+.wcard::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 2px;
+ background: linear-gradient(180deg, rgba(148, 196, 255, 0.5), rgba(148, 196, 255, 0.14));
+}
+
+.wcard[data-status="completed"]::before {
+ background: linear-gradient(180deg, rgba(var(--mint-rgb), 0.95), rgba(var(--mint-rgb), 0.3));
+ box-shadow: 0 0 12px rgba(var(--mint-rgb), 0.45);
+}
+
+.wcard[data-status="failed"]::before,
+.wcard[data-status="error"]::before,
+.wcard[data-status="cancelled"]::before,
+.wcard[data-status="canceled"]::before {
+ background: linear-gradient(180deg, rgba(var(--coral-rgb), 0.95), rgba(var(--coral-rgb), 0.3));
+ box-shadow: 0 0 12px rgba(var(--coral-rgb), 0.45);
+}
+
.wcard.expandable {
cursor: pointer;
}
.wcard.expandable:hover {
transform: translateY(-1px);
- background: rgba(255, 255, 255, 0.045);
- border-color: rgba(var(--cyan-rgb), 0.28);
+ background: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.07),
+ rgba(255, 255, 255, 0.035)
+ );
+ border-color: rgba(var(--cyan-rgb), 0.3);
box-shadow:
- 0 0 0 1px rgba(var(--cyan-rgb), 0.08),
- 0 14px 36px rgba(0, 0, 0, 0.4);
+ inset 0 1px 0 var(--edge-bright),
+ 0 14px 36px rgba(0, 0, 0, 0.42),
+ 0 0 0 1px rgba(var(--cyan-rgb), 0.08);
}
.wcard.working {
- border-color: rgba(var(--amber-rgb), 0.22);
+ border-color: rgba(var(--amber-rgb), 0.24);
}
.wcard.working::before {
- content: "";
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 2px;
background: linear-gradient(180deg, rgba(var(--amber-rgb), 0.95), rgba(var(--amber-rgb), 0.35));
box-shadow: 0 0 12px rgba(var(--amber-rgb), 0.55);
}
@@ -1076,42 +1209,50 @@
.wcard-top {
display: flex;
align-items: center;
- gap: 8px;
- margin-bottom: 9px;
+ gap: var(--s-2);
+ margin-bottom: var(--s-2);
}
.wcard-top code {
margin-left: auto;
font-family: var(--font-mono);
- font-size: 9.5px;
- letter-spacing: 0.04em;
+ font-size: var(--t-2xs);
+ letter-spacing: 0;
color: var(--muted);
+ opacity: 0.8;
}
+/* The task itself is the hero of the card. Clamped so one verbose Hermes goal
+ cannot grow a card tall enough to push the rest of the stream off-screen —
+ the full text is one click away in the reader. */
.wcard-task {
margin: 0;
- font-size: 13.5px;
- font-weight: 550;
- line-height: 1.4;
- letter-spacing: -0.005em;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ font-size: var(--t-base);
+ font-weight: var(--w-semi);
+ line-height: var(--lh-snug);
+ letter-spacing: var(--tr-snug);
color: var(--text);
}
.wcard-preview {
- margin-top: 7px;
+ margin-top: var(--s-2);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
- font-size: 11.5px;
- line-height: 1.5;
+ font-size: var(--t-sm);
+ line-height: var(--lh-body);
color: var(--muted);
}
.wcard-progress {
- margin-top: 11px;
+ margin-top: var(--s-3);
height: 2px;
- border-radius: 999px;
+ border-radius: var(--r-pill);
background: rgba(var(--amber-rgb), 0.14);
overflow: hidden;
}
@@ -1120,7 +1261,7 @@
display: block;
height: 100%;
width: 40%;
- border-radius: 999px;
+ border-radius: var(--r-pill);
background: linear-gradient(90deg, rgba(var(--amber-rgb), 0.4), var(--amber));
box-shadow: 0 0 8px rgba(var(--amber-rgb), 0.6);
animation: work-progress 1.8s ease-in-out infinite;
@@ -1136,9 +1277,9 @@
.activity-now {
display: flex;
align-items: center;
- gap: 8px;
- margin-top: 10px;
- font-size: 11.5px;
+ gap: var(--s-2);
+ margin-top: var(--s-3);
+ font-size: var(--t-sm);
color: var(--text-soft);
}
@@ -1164,11 +1305,11 @@
}
.activity-notes {
- margin: 8px 0 0;
- padding-left: 9px;
- border-left: 2px solid rgba(var(--cyan-rgb), 0.25);
- font-size: 11px;
- line-height: 1.5;
+ margin: var(--s-2) 0 0;
+ padding-left: var(--s-2);
+ border-left: 2px solid rgba(var(--accent-rgb), 0.3);
+ font-size: var(--t-xs);
+ line-height: var(--lh-body);
font-style: italic;
color: var(--muted);
display: -webkit-box;
@@ -1180,9 +1321,9 @@
.approval-card,
.interaction-card {
display: grid;
- gap: 7px;
- margin-top: 10px;
- padding: 10px;
+ gap: var(--s-2);
+ margin-top: var(--s-3);
+ padding: var(--s-3);
border-radius: var(--r-sm);
color: var(--text-soft);
background: rgba(var(--amber-rgb), 0.08);
@@ -1196,22 +1337,22 @@
.interaction-card strong {
color: var(--violet-soft);
- font-size: 10px;
- letter-spacing: 0.09em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-label);
text-transform: uppercase;
}
.interaction-card p {
margin: 0;
color: var(--text-soft);
- font-size: 10px;
- line-height: 1.45;
+ font-size: var(--t-xs);
+ line-height: var(--lh-snug);
}
.approval-card strong {
color: var(--amber);
- font-size: 10px;
- letter-spacing: 0.09em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-label);
text-transform: uppercase;
}
@@ -1220,13 +1361,14 @@
.approval-card small {
margin: 0;
overflow-wrap: anywhere;
- font-size: 10px;
- line-height: 1.45;
+ font-size: var(--t-xs);
+ line-height: var(--lh-snug);
}
.approval-card code {
max-height: 58px;
overflow: auto;
+ font-family: var(--font-mono);
color: var(--text);
}
@@ -1237,22 +1379,23 @@
.approval-actions {
display: flex;
flex-wrap: wrap;
- gap: 5px;
+ gap: var(--s-1);
}
.approval-actions button {
- padding: 5px 7px;
- border-radius: 5px;
+ padding: var(--s-1) var(--s-2);
+ border-radius: var(--r-xs);
color: var(--text-soft);
background: rgba(var(--cyan-rgb), 0.08);
box-shadow: inset 0 0 0 1px rgba(var(--cyan-rgb), 0.18);
- font-family: var(--font-mono);
- font-size: 8.5px;
+ font-size: var(--t-2xs);
+ font-weight: var(--w-medium);
+ transition: color var(--dur-fast) ease, background var(--dur-fast) ease;
}
.approval-actions button:hover:not(:disabled) {
color: var(--cyan-bright);
- background: rgba(var(--cyan-rgb), 0.14);
+ background: rgba(var(--cyan-rgb), 0.16);
}
.approval-actions button.always {
@@ -1268,7 +1411,7 @@
}
.activity {
- margin-top: 10px;
+ margin-top: var(--s-3);
}
/* Full-width strip (not a small pill): a comfortable target for the finger
@@ -1277,23 +1420,22 @@
display: flex;
width: 100%;
align-items: center;
- gap: 7px;
- padding: 8px 11px;
+ gap: var(--s-2);
+ padding: var(--s-2) var(--s-3);
border-radius: var(--r-sm);
- font-family: var(--font-mono);
- font-size: 9.5px;
- font-weight: 500;
- letter-spacing: 0.1em;
- text-transform: uppercase;
+ font-size: var(--t-xs);
+ font-weight: var(--w-medium);
+ letter-spacing: 0;
text-align: left;
color: var(--text-soft);
- background: rgba(var(--cyan-rgb), 0.06);
- box-shadow: inset 0 0 0 1px rgba(var(--cyan-rgb), 0.14);
- transition: background 0.15s ease, color 0.15s ease;
+ background: rgba(var(--cyan-rgb), 0.05);
+ box-shadow: inset 0 0 0 1px rgba(var(--cyan-rgb), 0.12);
+ transition: background var(--dur-fast) ease, color var(--dur-fast) ease;
}
.activity-toggle .chev {
margin-left: auto;
+ transition: transform var(--dur-fast) ease;
}
.activity-toggle:hover {
@@ -1301,22 +1443,18 @@
background: rgba(var(--cyan-rgb), 0.12);
}
-.activity-toggle .chev {
- transition: transform 0.18s ease;
-}
-
.activity-toggle.open .chev {
transform: rotate(180deg);
}
.activity-timeline {
list-style: none;
- margin: 9px 0 0;
- padding: 0 2px 0 0;
+ margin: var(--s-2) 0 0;
+ padding: 0 var(--s-05) 0 0;
display: flex;
flex-direction: column;
- gap: 5px;
- animation: timeline-in 0.2s ease;
+ gap: var(--s-1);
+ animation: timeline-in var(--dur-mid) ease;
/* Long runs (30+ steps) scroll inside the card instead of elongating it. */
max-height: 236px;
overflow-y: auto;
@@ -1330,11 +1468,11 @@
.activity-step {
display: flex;
align-items: center;
- gap: 9px;
- padding: 7px 9px;
+ gap: var(--s-2);
+ padding: var(--s-2);
border-radius: var(--r-sm);
- background: rgba(255, 255, 255, 0.02);
- box-shadow: inset 0 0 0 1px rgba(148, 196, 255, 0.06);
+ background: rgba(255, 255, 255, 0.022);
+ box-shadow: inset 0 0 0 1px rgba(148, 196, 255, 0.055);
}
.activity-step .step-icon {
@@ -1343,7 +1481,7 @@
place-items: center;
width: 24px;
height: 24px;
- border-radius: 7px;
+ border-radius: var(--r-xs);
color: var(--text-soft);
background: rgba(148, 196, 255, 0.08);
}
@@ -1373,8 +1511,8 @@
}
.step-tool {
- font-size: 11.5px;
- font-weight: 550;
+ font-size: var(--t-sm);
+ font-weight: var(--w-medium);
color: var(--text);
text-transform: capitalize;
}
@@ -1384,7 +1522,7 @@
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--font-mono);
- font-size: 9.5px;
+ font-size: var(--t-2xs);
color: var(--muted);
}
@@ -1392,13 +1530,13 @@
flex: 0 0 auto;
display: inline-flex;
align-items: center;
- gap: 6px;
+ gap: var(--s-2);
}
.step-meta em {
font-family: var(--font-mono);
font-style: normal;
- font-size: 9px;
+ font-size: var(--t-2xs);
color: var(--muted);
}
@@ -1428,7 +1566,7 @@
display: flex;
align-items: center;
justify-content: center;
- padding: 2px 8px 0;
+ padding: var(--s-05) var(--s-2) 0;
min-height: 20px;
overflow: hidden;
}
@@ -1437,10 +1575,10 @@
flex: 0 0 auto;
white-space: nowrap;
font-family: var(--font-mono);
- font-size: 9px;
- letter-spacing: 0.08em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-label);
color: var(--muted);
- opacity: 0.55;
+ opacity: 0.5;
}
.deck-foot .build-meta a {
diff --git a/src/styles/fx.css b/src/styles/fx.css
index 44bd6d2..8654691 100644
--- a/src/styles/fx.css
+++ b/src/styles/fx.css
@@ -153,12 +153,12 @@
z-index: 3;
display: inline-flex;
align-items: center;
- padding: 3px 9px;
- border-radius: 999px;
+ padding: var(--s-05) var(--s-2);
+ border-radius: var(--r-pill);
font-family: var(--font-mono);
- font-size: 8px;
- font-weight: 700;
- letter-spacing: 0.16em;
+ font-size: var(--t-2xs);
+ font-weight: var(--w-bold);
+ letter-spacing: var(--tr-caps);
text-transform: uppercase;
color: #1c1305;
background: linear-gradient(180deg, #ffd589, #f0a93f);
@@ -198,8 +198,8 @@
0 0 22px rgba(var(--cyan-rgb), 0.15);
backdrop-filter: blur(14px);
font-family: var(--font-mono);
- font-size: 9px;
- letter-spacing: 0.12em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-label);
animation: wake-reason-in 0.22s ease both;
}
@@ -341,25 +341,26 @@
position: absolute;
font-family: var(--font-display, var(--font-mono));
font-style: normal;
- font-weight: 700;
+ font-weight: var(--w-bold);
color: rgba(var(--violet-rgb), 0.95);
text-shadow: 0 0 14px rgba(var(--violet-rgb), 0.65);
opacity: 0;
animation: nap-z 4.8s ease-out infinite;
}
+/* Ascending ramp — the three Zs drift up and grow as they rise. */
.nap-zzz i:nth-child(1) {
- font-size: 13px;
+ font-size: var(--t-md);
animation-delay: 0s;
}
.nap-zzz i:nth-child(2) {
- font-size: 17px;
+ font-size: var(--t-lg);
animation-delay: 1.6s;
}
.nap-zzz i:nth-child(3) {
- font-size: 21px;
+ font-size: var(--t-xl);
animation-delay: 3.2s;
}
@@ -420,12 +421,12 @@
top: 22px;
left: 0;
transform: translateX(-50%);
- padding: 2px 8px;
- border-radius: 999px;
+ padding: var(--s-05) var(--s-2);
+ border-radius: var(--r-pill);
font-family: var(--font-mono);
- font-size: 9px;
- font-weight: 700;
- letter-spacing: 0.18em;
+ font-size: var(--t-2xs);
+ font-weight: var(--w-bold);
+ letter-spacing: var(--tr-caps);
white-space: nowrap;
background: rgba(10, 16, 28, 0.78);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
diff --git a/src/styles/hud.css b/src/styles/hud.css
index 3324969..aed020c 100644
--- a/src/styles/hud.css
+++ b/src/styles/hud.css
@@ -3,6 +3,11 @@
The window is transparent + click-through; only `.hud-hit` elements
(and open overlays) accept the pointer. Everything else lets you keep
working in the apps underneath.
+
+ Material rule: every island (tasks, comms, camera, caption, orb rail)
+ draws from ONE glass recipe in tokens.css — `--hud-fill`, `--hud-edge`,
+ `--hud-inner`. Five separate recipes is what used to make the HUD read as
+ scattered popups instead of one system.
===================================================================== */
html.hud-mode,
@@ -30,7 +35,7 @@ html:not(.hud-mode) .reader-backdrop,
html:not(.hud-mode) .history-backdrop,
html:not(.hud-mode) .match-backdrop,
html:not(.hud-mode) .setup-backdrop {
- border-radius: 14px;
+ border-radius: var(--r-md);
}
/* ===== Mode transitions ===== */
@@ -49,7 +54,7 @@ html:not(.hud-mode) .setup-backdrop {
/* Exiting HUD: the deck mounts invisible and fades in as the window bounds
are restored (the hold hides the resize). */
.deck.deck-entering {
- animation: deck-in 0.38s cubic-bezier(0.2, 0.8, 0.2, 1) 0.22s backwards;
+ animation: deck-in 0.38s var(--ease-out) 0.22s backwards;
}
@keyframes deck-in {
@@ -67,6 +72,15 @@ html:not(.hud-mode) .setup-backdrop {
pointer-events: none;
}
+/* ===== Shared HUD surfaces ===== */
+.hud-surface {
+ background: var(--hud-fill);
+ border: 1px solid var(--hud-edge);
+ box-shadow:
+ var(--hud-inner),
+ var(--hud-lift);
+}
+
/* ===== Orb cluster (bottom-right) ===== */
.hud-orb-cluster {
position: absolute;
@@ -82,10 +96,10 @@ html:not(.hud-mode) .setup-backdrop {
/* Right-aligned so the orb hugs the corner deterministically — the
control column anchors to its left edge regardless of caption width. */
align-items: flex-end;
- gap: 10px;
+ gap: var(--s-3);
/* Delayed + backwards-filled so elements stay invisible until the window
has finished jumping to fullscreen bounds. */
- animation: hud-rise 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) 0.16s backwards;
+ animation: hud-rise 0.5s var(--ease-out) 0.16s backwards;
}
.hud-orb.orb-stage {
@@ -93,6 +107,12 @@ html:not(.hud-mode) .setup-backdrop {
height: 190px;
}
+/* No scrim behind the reactor. There used to be a blurred 266px dark radial
+ here to give the additive glow something to sit against over bright
+ wallpapers, but on a dark desktop it reads as exactly what it is: a large
+ soft shadow blob around the orb. The reactor's own strokes carry enough
+ contrast, and a soft wash is the one thing this design does not use. */
+
.hud-shell.awake .orb-ring { opacity: 1; }
.hud-shell.awake .orb-radar { opacity: 0.7; }
.hud-shell.awake .hud-orb { animation: orb-ignite 0.9s ease; }
@@ -102,35 +122,74 @@ html:not(.hud-mode) .setup-backdrop {
opacity: 0.72;
}
+/* ===== Caption =====
+ In HUD mode this pill IS Iris's presence — it is the only place her voice
+ appears. It used to be a 13px rounded rect indistinguishable from a
+ tooltip; now it carries real typographic weight and a state light, so your
+ eye goes to it the moment she speaks. */
.hud-caption {
/* Centered above the reactor: the orb is right-aligned in the cluster, so
its center sits 95px from the cluster's right edge. */
position: absolute;
- bottom: calc(100% + 10px);
+ bottom: calc(100% + var(--s-2));
right: 95px;
transform: translateX(50%);
width: max-content;
- max-width: 240px; /* stays fully on-screen when centered on the orb */
- padding: 8px 15px;
- border-radius: 14px;
- background: linear-gradient(180deg, rgba(26, 36, 57, 0.68), rgba(10, 16, 28, 0.72));
- border: 1px solid rgba(255, 255, 255, 0.18);
+ /* Hard ceiling, not taste: centered on an orb whose center sits 121px from
+ the screen edge (26px margin + 95px), the pill's right edge lands at
+ `edge - 121 + width/2`, so anything over ~226px hangs off-screen. */
+ max-width: 224px;
+ display: flex;
+ align-items: flex-start;
+ gap: var(--s-2);
+ padding: var(--s-3) var(--s-4);
+ border-radius: var(--r-md);
+ background: var(--hud-fill-strong);
+ border: 1px solid var(--hud-edge);
box-shadow:
- inset 0 1px 0 rgba(255, 255, 255, 0.12),
- 0 1px 8px rgba(0, 0, 0, 0.18);
- font-size: 13px;
- line-height: 1.45;
- text-align: center;
- color: #f2f7ff;
- text-shadow: 0 1px 3px rgba(0, 0, 0, 0.65);
+ var(--hud-inner),
+ 0 2px 14px rgba(0, 0, 0, 0.28),
+ 0 0 34px -10px rgba(var(--accent-rgb), 0.5);
+ font-size: var(--t-base);
+ font-weight: var(--w-medium);
+ line-height: var(--lh-snug);
+ letter-spacing: var(--tr-snug);
+ text-align: left;
+ color: var(--hud-text);
+ text-shadow: var(--hud-shadow-text);
+ transition: box-shadow var(--accent-ease);
+}
+
+/* Three lines: the pill is narrow (see max-width above) and Iris's replies are
+ whole sentences — two lines clipped most of them mid-thought. */
+.hud-caption-text {
display: -webkit-box;
- -webkit-line-clamp: 2;
+ -webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
+/* The state light: same color the orb is running, so the caption is visibly
+ part of the reactor rather than a floating label. */
+.hud-caption-dot {
+ flex: 0 0 auto;
+ width: 7px;
+ height: 7px;
+ margin-top: 6px;
+ border-radius: 50%;
+ background: rgba(var(--accent-rgb), 1);
+ box-shadow: 0 0 10px rgba(var(--accent-rgb), 0.9);
+ transition: background var(--accent-ease), box-shadow var(--accent-ease);
+ animation: caption-live 2.4s ease-in-out infinite;
+}
+
+@keyframes caption-live {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.45; }
+}
+
.hud-caption.dim {
- color: #c3cfe0;
+ color: var(--hud-text-soft);
}
/* Whisper-size states: the asleep hint ("Say Hey Iris") and transient status
@@ -138,19 +197,35 @@ html:not(.hud-mode) .setup-backdrop {
the full size above. Three lines allowed — the standby-with-Hermes message
is the longest and must never clip mid-sentence. */
.hud-caption.hint {
- max-width: 230px;
- padding: 4px 10px;
- border-radius: 9px;
- font-size: 9.5px;
- letter-spacing: 0.04em;
- opacity: 0.85;
+ max-width: 208px;
+ padding: var(--s-1) var(--s-3);
+ border-radius: var(--r-sm);
+ font-size: var(--t-2xs);
+ font-weight: var(--w-normal);
+ letter-spacing: var(--tr-label);
+ opacity: 0.9;
+ box-shadow: var(--hud-inner), var(--hud-lift);
+}
+
+.hud-caption.hint .hud-caption-text {
-webkit-line-clamp: 3;
}
-/* Control buttons: a vertical column to the LEFT of the orb, bottom-aligned
- with the camera's 100px Dock-clearance line — never under the Dock, no
- matter how many buttons or how wide the Dock gets. (Fixed-positioned, but
- hover still works: the column is a DOM child of the cluster.) */
+/* ===== Control rail =====
+ Was four free-floating circles; now ONE capsule that reads as a single
+ object clipped to the orb. Vertical column to the LEFT of the orb,
+ bottom-aligned with the camera's 100px Dock-clearance line — never under
+ the Dock, no matter how many buttons or how wide the Dock gets.
+ (Fixed-positioned, but hover still works: `:hover` is DOM-based, so the
+ pointer over this fixed child still matches the cluster ancestor — which is
+ what lets you travel from the orb onto the rail without it vanishing.)
+
+ Hidden at rest ON PURPOSE: the HUD floats over your desktop and must stay
+ visually silent until addressed, so the rail appears only when you hover the
+ reactor. That does mean there is no permanently visible way out of HUD mode,
+ which is deliberate rather than an oversight — the escape hatches are the
+ ⌥H hotkey, "Toggle Glass HUD" in the View menu, and "Exit Glass HUD" in the
+ menu-bar tray. Please don't make this visible-at-rest to "fix" that. */
.hud-controls {
position: fixed;
right: 230px; /* 26px margin + 190px orb + 14px gap */
@@ -158,7 +233,14 @@ html:not(.hud-mode) .setup-backdrop {
display: flex;
flex-direction: column;
align-items: center;
- gap: 8px;
+ gap: var(--s-05);
+ padding: var(--s-2) var(--s-1);
+ border-radius: var(--r-pill);
+ background: var(--hud-chip);
+ box-shadow:
+ var(--hud-inner),
+ inset 0 0 0 1px var(--hud-edge),
+ var(--hud-lift);
opacity: 0;
transform: translateX(6px);
transition: opacity 0.2s ease, transform 0.2s ease;
@@ -166,6 +248,8 @@ html:not(.hud-mode) .setup-backdrop {
.hud-orb-cluster:hover .hud-controls,
.hud-orb-cluster:focus-within .hud-controls,
+.hud-controls:hover,
+/* `.show` is driven by hand-tracking proximity to the reactor corner. */
.hud-controls.show {
opacity: 1;
transform: none;
@@ -177,41 +261,51 @@ html:not(.hud-mode) .setup-backdrop {
width: 32px;
height: 32px;
border-radius: 50%;
- color: #dbe6f5;
+ color: var(--hud-text-soft);
background: rgba(22, 30, 47, 0.68);
box-shadow:
- inset 0 1px 0 rgba(255, 255, 255, 0.14),
- inset 0 0 0 1px rgba(255, 255, 255, 0.16);
- transition: color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
+ var(--hud-inner),
+ inset 0 0 0 1px var(--hud-edge);
+ transition: color var(--dur-fast) ease, box-shadow var(--dur-fast) ease, background var(--dur-fast) ease;
+}
+
+/* Inside the rail the capsule already provides the container, so the buttons
+ drop their own rings — otherwise you get rings inside a ring. */
+.hud-controls .hud-btn {
+ background: transparent;
+ box-shadow: none;
+}
+
+.hud-controls .hud-btn:hover {
+ background: rgba(255, 255, 255, 0.12);
+ box-shadow: none;
}
.hud-btn:hover {
- transform: translateY(-1px);
color: var(--cyan-bright);
- box-shadow: inset 0 0 0 1px rgba(var(--cyan-rgb), 0.4);
}
-.hud-btn.danger:hover {
+.hud-controls .hud-btn.danger:hover {
color: var(--coral);
- box-shadow: inset 0 0 0 1px rgba(var(--coral-rgb), 0.45);
+ background: rgba(var(--coral-rgb), 0.18);
}
-.hud-btn.wake {
+.hud-controls .hud-btn.wake {
color: var(--cyan-bright);
- box-shadow: inset 0 0 0 1px rgba(var(--cyan-rgb), 0.4);
+ background: rgba(var(--cyan-rgb), 0.16);
}
-.hud-btn.muted {
+.hud-controls .hud-btn.muted {
color: var(--amber);
- box-shadow: inset 0 0 0 1px rgba(var(--amber-rgb), 0.45);
+ background: rgba(var(--amber-rgb), 0.18);
}
-.hud-btn.active {
+.hud-controls .hud-btn.active {
color: #06222b;
background: linear-gradient(180deg, var(--cyan-bright), var(--cyan));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.45),
- 0 0 18px rgba(var(--cyan-rgb), 0.4);
+ 0 0 16px rgba(var(--cyan-rgb), 0.45);
}
/* ===== Slim work stream (top-right) ===== */
@@ -223,8 +317,8 @@ html:not(.hud-mode) .setup-backdrop {
display: flex;
flex-direction: column;
align-items: flex-end;
- gap: 8px;
- animation: hud-slide 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) 0.16s backwards;
+ gap: var(--s-2);
+ animation: hud-slide 0.45s var(--ease-out) 0.16s backwards;
}
.hud-work {
@@ -234,9 +328,9 @@ html:not(.hud-mode) .setup-backdrop {
overflow-y: auto;
display: flex;
flex-direction: column;
- gap: 8px;
+ gap: var(--s-2);
scrollbar-width: none;
- animation: hud-rise 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
+ animation: hud-rise 0.25s var(--ease-out);
}
.hud-work::-webkit-scrollbar {
@@ -254,76 +348,87 @@ html:not(.hud-mode) .setup-backdrop {
z-index: 40; /* clickable above the Neural Map canvas */
display: flex;
flex-direction: column;
- gap: 10px;
+ gap: var(--s-2);
width: 300px;
- animation: hud-rise 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) 0.16s backwards;
+ animation: hud-rise 0.5s var(--ease-out) 0.16s backwards;
}
.hud-camera .camera-frame {
margin: 0;
aspect-ratio: 4 / 3;
- border-radius: var(--r-card);
+ border-radius: var(--r-md);
box-shadow:
- inset 0 0 0 1px var(--hairline-strong),
- 0 1px 8px rgba(0, 0, 0, 0.2);
+ inset 0 0 0 1px var(--hud-edge),
+ var(--hud-lift);
}
+/* ===== HUD chips (Tasks / Comms toggles) ===== */
.hud-comms-toggle {
align-self: flex-start;
display: inline-flex;
align-items: center;
- gap: 7px;
- padding: 6px 12px;
- border-radius: 999px;
- font-family: var(--font-mono);
- font-size: 9.5px;
- font-weight: 500;
- letter-spacing: 0.18em;
- text-transform: uppercase;
- color: #dbe6f5;
- background: rgba(22, 30, 47, 0.68);
+ gap: var(--s-2);
+ padding: var(--s-1) var(--s-3);
+ border-radius: var(--r-pill);
+ font-size: var(--t-xs);
+ font-weight: var(--w-semi);
+ letter-spacing: var(--tr-snug);
+ color: var(--hud-text);
+ text-shadow: var(--hud-shadow-text-soft);
+ background: var(--hud-chip);
box-shadow:
- inset 0 1px 0 rgba(255, 255, 255, 0.14),
- inset 0 0 0 1px rgba(255, 255, 255, 0.16);
- transition: color 0.15s ease, box-shadow 0.15s ease;
+ var(--hud-inner),
+ inset 0 0 0 1px var(--hud-edge);
+ transition: color var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}
.hud-comms-toggle:hover {
color: var(--cyan-bright);
box-shadow:
- inset 0 1px 0 rgba(255, 255, 255, 0.14),
- inset 0 0 0 1px rgba(var(--cyan-rgb), 0.4);
+ var(--hud-inner),
+ inset 0 0 0 1px rgba(var(--cyan-rgb), 0.45);
}
.hud-comms-toggle .count {
display: inline-grid;
place-items: center;
- min-width: 16px;
- height: 16px;
- padding: 0 5px;
- border-radius: 999px;
- font-size: 9px;
+ min-width: 17px;
+ height: 17px;
+ padding: 0 var(--s-1);
+ border-radius: var(--r-pill);
+ font-family: var(--font-mono);
+ font-size: var(--t-2xs);
color: var(--cyan-bright);
- background: rgba(var(--cyan-rgb), 0.14);
+ background: rgba(var(--cyan-rgb), 0.18);
+ text-shadow: none;
}
.hud-comms-toggle .chev {
- transition: transform 0.18s ease;
+ transition: transform var(--dur-fast) ease;
}
.hud-comms-toggle.open .chev {
transform: rotate(180deg);
}
+/* ===== Comms =====
+ The bubbles used to float loose on the wallpaper, which read as stray
+ notifications. They now live inside a real pane. */
.hud-comms {
max-height: 34vh;
overflow-y: auto;
display: flex;
flex-direction: column;
- gap: 8px;
- padding: 2px;
+ gap: var(--s-2);
+ padding: var(--s-3);
+ border-radius: var(--r-md);
+ background: linear-gradient(180deg, rgba(18, 26, 44, 0.42), rgba(8, 13, 24, 0.46));
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.1),
+ var(--hud-lift);
scrollbar-width: none;
- animation: hud-rise 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
+ animation: hud-rise 0.25s var(--ease-out);
}
.hud-comms::-webkit-scrollbar {
@@ -333,26 +438,26 @@ html:not(.hud-mode) .setup-backdrop {
/* Keep the left/right speaker offset readable: Iris hugs the left edge, you
hug the right, with clear opposite-side indents. */
.hud-comms .bubble {
- max-width: 84%;
+ max-width: 86%;
}
/* ===== HUD glass surfaces =====
The OS behind a transparent window can't be backdrop-blurred, so readable
glass here = a light (~60%) tinted fill you can see the desktop through, a
bright 1px edge highlight to define the pane, and text shadows so text
- survives worst-case bright wallpapers (per NN/g glassmorphism guidance). */
-/* No big drop shadows on stacked HUD elements: overlapping shadows fuse into a
+ survives worst-case bright wallpapers (per NN/g glassmorphism guidance).
+ No big drop shadows on stacked HUD elements: overlapping shadows fuse into a
dark plate behind the whole column. A crisp edge + hairline is enough. */
.hud-mode .wcard {
- background: linear-gradient(180deg, rgba(26, 36, 57, 0.58), rgba(10, 16, 28, 0.6));
- border-color: rgba(255, 255, 255, 0.18);
+ background: var(--hud-fill);
+ border-color: var(--hud-edge);
box-shadow:
- inset 0 1px 0 rgba(255, 255, 255, 0.14),
+ var(--hud-inner),
0 1px 6px rgba(0, 0, 0, 0.16);
}
.hud-mode .wcard.expandable:hover {
- background: linear-gradient(180deg, rgba(33, 46, 72, 0.7), rgba(13, 20, 34, 0.72));
+ background: var(--hud-fill-strong);
border-color: rgba(var(--cyan-rgb), 0.45);
}
@@ -361,27 +466,27 @@ html:not(.hud-mode) .setup-backdrop {
}
.hud-mode .wcard-task {
- color: #f2f7ff;
- text-shadow: 0 1px 3px rgba(0, 0, 0, 0.65);
+ color: var(--hud-text);
+ text-shadow: var(--hud-shadow-text);
}
.hud-mode .wcard-preview {
- color: #c6d2e4;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
+ color: var(--hud-text-soft);
+ text-shadow: var(--hud-shadow-text-soft);
}
.hud-mode .wcard-top code {
- color: #a7b7cd;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
+ color: var(--hud-text-dim);
+ text-shadow: var(--hud-shadow-text-soft);
}
.hud-mode .badge {
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
+ text-shadow: var(--hud-shadow-text-soft);
}
.hud-mode .step-tool,
.hud-mode .activity-now {
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
+ text-shadow: var(--hud-shadow-text-soft);
}
.hud-mode .step-detail {
@@ -394,42 +499,48 @@ html:not(.hud-mode) .setup-backdrop {
}
.hud-mode .bubble {
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
+ text-shadow: var(--hud-shadow-text-soft);
}
.hud-mode .bubble.iris {
- background: rgba(15, 30, 43, 0.6);
+ background: rgba(15, 30, 43, 0.62);
border-color: rgba(var(--cyan-rgb), 0.32);
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 6px rgba(0, 0, 0, 0.14);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.hud-mode .bubble.self {
- background: rgba(22, 29, 45, 0.6);
+ background: rgba(24, 31, 48, 0.62);
border-color: rgba(255, 255, 255, 0.18);
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 6px rgba(0, 0, 0, 0.14);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.hud-mode .bubble .who {
text-shadow: none;
}
+/* --muted is tuned for the deck's near-black panels; on HUD glass over a
+ bright wallpaper it drops close to illegible. */
+.hud-mode .bubble.self .who {
+ color: var(--hud-text-dim);
+}
+
/* The open reader/history/chooser use the SAME glass language as the Hermes
cards (slightly stronger fill for dense text, plus text shadows). Settings
stays near-opaque — forms on live glass are unreadable. */
.hud-mode .reader-card,
.hud-mode .history-card,
.hud-mode .match-card {
- background: linear-gradient(180deg, rgba(26, 36, 57, 0.68), rgba(10, 16, 28, 0.72));
- border-color: rgba(255, 255, 255, 0.18);
+ background: var(--hud-fill-strong);
+ border-color: var(--hud-edge);
box-shadow:
- inset 0 1px 0 rgba(255, 255, 255, 0.14),
+ var(--hud-inner),
0 30px 90px rgba(0, 0, 0, 0.5);
}
.hud-mode .reader-title,
.hud-mode .markdown-body,
.hud-mode .match-copy strong {
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
+ text-shadow: var(--hud-shadow-text-soft);
}
.hud-mode .markdown-body {
@@ -471,42 +582,44 @@ html:not(.hud-mode) .setup-backdrop {
display: flex;
flex-direction: column;
align-items: center;
- gap: 3px;
+ gap: var(--s-1);
width: 300px;
height: fit-content;
max-height: max(0px, calc(58vh - 408px));
margin-top: auto;
margin-bottom: auto;
overflow: hidden;
- padding: 10px 14px 9px;
- border-radius: 14px;
+ padding: var(--s-3) var(--s-4);
+ border-radius: var(--r-md);
background: rgba(8, 12, 22, 0.94);
border: 1px solid var(--hairline-strong);
- box-shadow: 0 24px 70px rgba(0, 0, 0, 0.55);
- animation: hud-rise 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
+ box-shadow: var(--e-3);
+ animation: hud-rise 0.4s var(--ease-out);
}
.boot.compact .boot-title {
- font-size: 12.5px;
+ font-size: var(--t-md);
}
.boot.compact .boot-sub {
- font-size: 6.5px;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-caps);
+ margin-right: calc(var(--tr-caps) * -1);
}
.boot.compact .boot-log {
width: 100%;
- margin-top: 2px;
- font-size: 9px;
+ margin-top: var(--s-05);
+ font-size: var(--t-2xs);
}
.boot.compact .boot-line {
- gap: 7px;
+ gap: var(--s-2);
padding: 1.5px 0;
}
.boot.compact .boot-line .boot-state {
- font-size: 8px;
+ font-size: var(--t-2xs);
}
.boot.compact .boot-bar {
@@ -525,7 +638,9 @@ html:not(.hud-mode) .setup-backdrop {
@media (prefers-reduced-motion: reduce) {
.hud-orb-cluster,
- .hud-work {
+ .hud-work,
+ .hud-comms,
+ .hud-caption-dot {
animation: none;
}
}
diff --git a/src/styles/overlays.css b/src/styles/overlays.css
index 80cc4c2..ac8d2fb 100644
--- a/src/styles/overlays.css
+++ b/src/styles/overlays.css
@@ -9,7 +9,7 @@
inset: 0;
display: grid;
place-items: center;
- padding: 40px;
+ padding: var(--s-8);
background: rgba(2, 5, 10, 0.55);
backdrop-filter: blur(10px);
animation: overlay-fade 0.2s ease;
@@ -28,43 +28,43 @@
left: 50%;
z-index: 70;
width: min(520px, calc(100vw - 32px));
- padding: 14px;
+ padding: var(--s-4);
transform: translateX(-50%);
- border-radius: var(--r-card);
+ border-radius: var(--r-md);
color: var(--text-soft);
background: var(--panel-strong);
box-shadow:
inset 0 0 0 1px rgba(var(--amber-rgb), 0.4),
- var(--shadow-panel);
+ var(--e-4);
backdrop-filter: blur(20px) saturate(1.15);
- animation: approval-prompt-in 0.24s ease both;
+ animation: approval-prompt-in var(--dur-mid) ease both;
}
.approval-prompt header {
display: flex;
align-items: flex-start;
- gap: 10px;
+ gap: var(--s-3);
color: var(--amber);
}
.approval-prompt header div {
min-width: 0;
display: grid;
- gap: 3px;
+ gap: var(--s-05);
}
.approval-prompt header strong {
font-family: var(--font-display);
- font-size: 13px;
- letter-spacing: 0.08em;
+ font-size: var(--t-md);
+ letter-spacing: var(--tr-label);
text-transform: uppercase;
}
.approval-prompt header span {
overflow: hidden;
color: var(--text);
- font-size: 12px;
- line-height: 1.4;
+ font-size: var(--t-sm);
+ line-height: var(--lh-snug);
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -72,19 +72,20 @@
.approval-prompt > code,
.approval-prompt > p {
display: block;
- margin: 10px 0 0;
+ margin: var(--s-3) 0 0;
overflow-wrap: anywhere;
- font-size: 11px;
- line-height: 1.5;
+ font-size: var(--t-xs);
+ line-height: var(--lh-body);
}
.approval-prompt > code {
max-height: 90px;
overflow: auto;
- padding: 9px;
+ padding: var(--s-2);
border-radius: var(--r-sm);
color: var(--text);
- background: rgba(0, 0, 0, 0.22);
+ font-family: var(--font-mono);
+ background: rgba(0, 0, 0, 0.24);
}
.approval-prompt .approval-missing {
@@ -94,19 +95,19 @@
.approval-prompt-actions {
display: flex;
flex-wrap: wrap;
- gap: 7px;
- margin-top: 12px;
+ gap: var(--s-2);
+ margin-top: var(--s-3);
}
.approval-prompt-actions button {
- padding: 7px 10px;
- border-radius: 7px;
+ padding: var(--s-2) var(--s-3);
+ border-radius: var(--r-xs);
color: var(--text-soft);
background: rgba(var(--cyan-rgb), 0.09);
box-shadow: inset 0 0 0 1px rgba(var(--cyan-rgb), 0.2);
- font-family: var(--font-mono);
- font-size: 9px;
- letter-spacing: 0.04em;
+ font-size: var(--t-xs);
+ font-weight: var(--w-medium);
+ transition: color var(--dur-fast) ease, background var(--dur-fast) ease;
}
.approval-prompt-actions button:hover:not(:disabled) {
@@ -131,9 +132,9 @@
.approval-prompt small {
display: block;
- margin-top: 8px;
+ margin-top: var(--s-2);
color: var(--coral);
- font-size: 10px;
+ font-size: var(--t-xs);
}
@keyframes approval-prompt-in {
@@ -149,28 +150,28 @@
width: min(620px, calc(100vw - 32px));
max-height: calc(100vh - 104px);
overflow: auto;
- padding: 16px;
+ padding: var(--s-4);
transform: translateX(-50%);
- border-radius: var(--r-card);
+ border-radius: var(--r-md);
color: var(--text-soft);
background: var(--panel-strong);
box-shadow:
inset 0 0 0 1px rgba(var(--violet-rgb), 0.4),
- var(--shadow-float);
+ var(--e-4);
backdrop-filter: blur(22px) saturate(1.15);
- animation: approval-prompt-in 0.24s ease both;
+ animation: approval-prompt-in var(--dur-mid) ease both;
}
.hermes-interaction-prompt.secure {
box-shadow:
inset 0 0 0 1px rgba(var(--amber-rgb), 0.45),
- var(--shadow-float);
+ var(--e-4);
}
.hermes-interaction-prompt header {
display: flex;
align-items: flex-start;
- gap: 10px;
+ gap: var(--s-3);
color: var(--violet-soft);
}
@@ -181,62 +182,65 @@
.hermes-interaction-prompt header div {
min-width: 0;
display: grid;
- gap: 3px;
+ gap: var(--s-05);
}
.hermes-interaction-prompt header strong {
font-family: var(--font-display);
- font-size: 13px;
- letter-spacing: 0.08em;
+ font-size: var(--t-md);
+ letter-spacing: var(--tr-label);
text-transform: uppercase;
}
.hermes-interaction-prompt header span {
overflow: hidden;
color: var(--text);
- font-size: 11px;
+ font-size: var(--t-xs);
text-overflow: ellipsis;
white-space: nowrap;
}
.interaction-question {
- margin: 14px 0 0;
+ margin: var(--s-4) 0 0;
color: var(--text);
- font-size: 14px;
- font-weight: 600;
- line-height: 1.55;
+ font-size: var(--t-base);
+ font-weight: var(--w-semi);
+ line-height: var(--lh-body);
}
.hermes-interaction-prompt > code {
display: block;
max-height: 90px;
- margin-top: 10px;
+ margin-top: var(--s-3);
overflow: auto;
- padding: 9px;
+ padding: var(--s-2);
border-radius: var(--r-sm);
color: var(--text);
+ font-family: var(--font-mono);
background: rgba(0, 0, 0, 0.24);
overflow-wrap: anywhere;
- font-size: 10px;
+ font-size: var(--t-xs);
}
.interaction-choices {
display: grid;
- gap: 7px;
- margin-top: 13px;
+ gap: var(--s-2);
+ margin-top: var(--s-3);
}
.interaction-choices button {
display: flex;
align-items: center;
- gap: 10px;
+ gap: var(--s-3);
width: 100%;
- padding: 9px 10px;
- border-radius: 8px;
+ padding: var(--s-2) var(--s-3);
+ border-radius: var(--r-xs);
color: var(--text-soft);
text-align: left;
+ font-size: var(--t-md);
background: rgba(255, 255, 255, 0.025);
box-shadow: inset 0 0 0 1px rgba(148, 196, 255, 0.1);
+ transition: color var(--dur-fast) ease, background var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}
.interaction-choices button:hover:not(:disabled),
@@ -256,23 +260,23 @@
width: 22px;
height: 22px;
place-items: center;
- border-radius: 5px;
+ border-radius: var(--r-xs);
color: var(--muted);
background: rgba(255, 255, 255, 0.05);
font-family: var(--font-mono);
- font-size: 9px;
+ font-size: var(--t-2xs);
}
.hermes-interaction-prompt label {
display: grid;
- gap: 6px;
- margin-top: 12px;
+ gap: var(--s-2);
+ margin-top: var(--s-3);
}
.hermes-interaction-prompt label > span {
font-family: var(--font-mono);
- font-size: 9px;
- letter-spacing: 0.08em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-label);
text-transform: uppercase;
color: var(--muted);
}
@@ -280,14 +284,14 @@
.hermes-interaction-prompt textarea,
.hermes-interaction-prompt input {
width: 100%;
- padding: 9px 10px;
+ padding: var(--s-2) var(--s-3);
border: 0;
- border-radius: 8px;
+ border-radius: var(--r-xs);
outline: none;
color: var(--text);
background: rgba(0, 0, 0, 0.26);
box-shadow: inset 0 0 0 1px var(--hairline-strong);
- font: 12px/1.5 var(--font-ui);
+ font: var(--t-md) / var(--lh-body) var(--font-ui);
resize: vertical;
}
@@ -299,15 +303,15 @@
.hermes-interaction-prompt footer {
display: flex;
justify-content: flex-end;
- gap: 8px;
- margin-top: 14px;
+ gap: var(--s-2);
+ margin-top: var(--s-4);
}
.hermes-interaction-prompt footer button {
- padding: 7px 12px;
- border-radius: 7px;
- font-family: var(--font-mono);
- font-size: 9px;
+ padding: var(--s-2) var(--s-3);
+ border-radius: var(--r-xs);
+ font-size: var(--t-xs);
+ font-weight: var(--w-medium);
}
.hermes-interaction-prompt footer .skip {
@@ -326,9 +330,9 @@
.hermes-interaction-prompt small {
display: block;
- margin-top: 9px;
+ margin-top: var(--s-2);
color: var(--amber);
- font-size: 9px;
+ font-size: var(--t-xs);
}
.hermes-interaction-prompt small.error {
@@ -352,9 +356,7 @@
.setup-card {
background: var(--panel-strong);
border: 1px solid var(--hairline-strong);
- box-shadow:
- inset 0 1px 0 var(--edge-light),
- var(--shadow-float);
+ box-shadow: var(--e-4);
overflow: hidden;
}
@@ -378,7 +380,7 @@
color: var(--text-soft);
background: rgba(255, 255, 255, 0.04);
box-shadow: inset 0 0 0 1px var(--hairline);
- transition: color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
+ transition: color var(--dur-fast) ease, background var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}
.reader-close:hover {
@@ -393,9 +395,9 @@
flex-direction: column;
width: min(820px, 92vw);
max-height: 84vh;
- border-radius: 20px;
+ border-radius: var(--r-xl);
transform: var(--reader-transform, translate(0, 0) scale(1));
- animation: overlay-pop 0.24s cubic-bezier(0.2, 0.8, 0.2, 1);
+ animation: overlay-pop var(--dur-mid) var(--ease-out);
will-change: transform;
}
@@ -404,7 +406,7 @@
}
.reader-card:not(.dragging) {
- transition: transform 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
+ transition: transform 0.22s var(--ease-out);
}
.reader-card.closing {
@@ -427,8 +429,8 @@
.reader-grab {
display: flex;
align-items: center;
- gap: 12px;
- padding: 15px 18px 12px;
+ gap: var(--s-3);
+ padding: var(--s-4) var(--s-5) var(--s-3);
cursor: grab;
touch-action: none;
border-bottom: 1px solid var(--hairline);
@@ -444,32 +446,33 @@
top: 7px;
width: 44px;
height: 4px;
- border-radius: 999px;
+ border-radius: var(--r-pill);
background: rgba(148, 196, 255, 0.18);
transform: translateX(-50%);
}
.reader-grab code {
font-family: var(--font-mono);
- font-size: 10px;
- letter-spacing: 0.04em;
+ font-size: var(--t-2xs);
+ letter-spacing: 0;
color: var(--muted);
}
.reader-title {
margin: 0;
- padding: 14px 22px 12px;
- font-size: 19px;
- font-weight: 600;
- letter-spacing: -0.012em;
- line-height: 1.35;
+ padding: var(--s-4) var(--s-6) var(--s-3);
+ font-size: var(--t-xl);
+ font-weight: var(--w-semi);
+ letter-spacing: var(--tr-tight);
+ line-height: var(--lh-tight);
color: var(--text);
+ text-wrap: balance;
}
/* Hermes tool-step timeline inside the reader (voice: "show the steps") */
.reader-steps {
flex: 0 0 auto;
- padding: 0 22px 12px;
+ padding: 0 var(--s-6) var(--s-3);
}
.reader-steps-list {
@@ -488,15 +491,15 @@
flex: 1;
min-height: 0;
overflow-y: auto;
- padding: 0 22px 10px;
+ padding: 0 var(--s-6) var(--s-3);
scrollbar-width: thin;
}
/* --- Markdown --- */
.markdown-body {
color: var(--text-soft);
- font-size: 13.5px;
- line-height: 1.7;
+ font-size: var(--t-base);
+ line-height: var(--lh-loose);
}
.markdown-body.error {
@@ -511,41 +514,41 @@
.markdown-body ol,
.markdown-body blockquote,
.markdown-body table {
- margin: 0 0 12px;
+ margin: 0 0 var(--s-3);
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3 {
- margin: 20px 0 10px;
+ margin: var(--s-5) 0 var(--s-3);
color: var(--text);
- font-weight: 600;
- letter-spacing: -0.01em;
- line-height: 1.25;
+ font-weight: var(--w-semi);
+ letter-spacing: var(--tr-tight);
+ line-height: var(--lh-tight);
}
-.markdown-body h1 { font-size: 21px; }
+.markdown-body h1 { font-size: var(--t-xl); }
.markdown-body h2 {
- font-size: 17px;
- padding-left: 11px;
+ font-size: var(--t-lg);
+ padding-left: var(--s-3);
border-left: 3px solid rgba(var(--cyan-rgb), 0.6);
}
-.markdown-body h3 { font-size: 15px; }
+.markdown-body h3 { font-size: var(--t-base); }
.markdown-body strong {
color: var(--text);
- font-weight: 600;
+ font-weight: var(--w-semi);
}
.markdown-body ul,
.markdown-body ol {
- padding-left: 22px;
+ padding-left: var(--s-6);
}
.markdown-body li {
- margin: 4px 0;
+ margin: var(--s-1) 0;
}
.markdown-body li::marker {
@@ -553,8 +556,8 @@
}
.markdown-body code {
- padding: 2px 6px;
- border-radius: 6px;
+ padding: 2px var(--s-1);
+ border-radius: var(--r-xs);
background: rgba(148, 196, 255, 0.09);
font-family: var(--font-mono);
font-size: 0.88em;
@@ -562,8 +565,8 @@
}
.markdown-body pre {
- margin: 12px 0;
- padding: 13px;
+ margin: var(--s-3) 0;
+ padding: var(--s-3);
overflow: auto;
border-radius: var(--r-sm);
background: rgba(3, 8, 16, 0.7);
@@ -577,7 +580,7 @@
}
.markdown-body blockquote {
- padding-left: 12px;
+ padding-left: var(--s-3);
border-left: 3px solid rgba(var(--violet-rgb), 0.5);
color: var(--muted);
}
@@ -595,27 +598,27 @@
.markdown-body table {
width: 100%;
border-collapse: collapse;
- font-size: 12.5px;
+ font-size: var(--t-md);
}
.markdown-body th {
color: var(--text);
- font-weight: 600;
+ font-weight: var(--w-semi);
}
.markdown-body th,
.markdown-body td {
- padding: 8px;
+ padding: var(--s-2);
border-bottom: 1px solid var(--hairline);
text-align: left;
}
.reader-hint {
- padding: 13px 22px 16px;
+ padding: var(--s-3) var(--s-6) var(--s-4);
border-top: 1px solid var(--hairline);
font-family: var(--font-mono);
- font-size: 9px;
- letter-spacing: 0.16em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-caps);
text-transform: uppercase;
color: var(--muted);
}
@@ -626,25 +629,24 @@
max-height: 84vh;
display: flex;
flex-direction: column;
- border-radius: 20px;
- animation: overlay-pop 0.24s cubic-bezier(0.2, 0.8, 0.2, 1);
+ border-radius: var(--r-xl);
+ animation: overlay-pop var(--dur-mid) var(--ease-out);
}
.history-head {
display: flex;
align-items: center;
- gap: 10px;
- padding: 16px 20px 13px;
+ gap: var(--s-3);
+ padding: var(--s-4) var(--s-5) var(--s-3);
border-bottom: 1px solid var(--hairline);
- font-family: var(--font-mono);
- font-size: 10.5px;
- letter-spacing: 0.22em;
- text-transform: uppercase;
- color: var(--muted);
+ font-size: var(--t-base);
+ font-weight: var(--w-semi);
+ letter-spacing: var(--tr-snug);
+ color: var(--text);
}
.history-head svg {
- color: rgba(var(--cyan-rgb), 0.66);
+ color: rgba(var(--accent-rgb), 0.8);
}
.history-head .reader-close { margin-left: auto; }
@@ -655,55 +657,55 @@
overflow-y: auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
- gap: 10px;
- padding: 14px 20px 20px;
+ gap: var(--s-3);
+ padding: var(--s-4) var(--s-5) var(--s-5);
}
/* ===== Ambiguous voice task chooser ===== */
.match-card {
width: min(620px, 92vw);
- border-radius: 20px;
- animation: overlay-pop 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
+ border-radius: var(--r-xl);
+ animation: overlay-pop 0.22s var(--ease-out);
}
.match-head {
display: flex;
align-items: center;
- gap: 12px;
- padding: 16px 18px 8px;
+ gap: var(--s-3);
+ padding: var(--s-4) var(--s-5) var(--s-2);
color: var(--text);
- font-size: 14px;
- font-weight: 600;
- letter-spacing: -0.005em;
+ font-size: var(--t-base);
+ font-weight: var(--w-semi);
+ letter-spacing: var(--tr-snug);
}
.match-head .reader-close { margin-left: auto; }
.match-query {
margin: 0;
- padding: 0 18px 12px;
+ padding: 0 var(--s-5) var(--s-3);
color: var(--muted);
- font-size: 12px;
+ font-size: var(--t-sm);
}
.match-list {
display: grid;
- gap: 8px;
- padding: 0 18px 14px;
+ gap: var(--s-2);
+ padding: 0 var(--s-5) var(--s-4);
}
.match-option {
display: flex;
align-items: center;
- gap: 12px;
+ gap: var(--s-3);
min-width: 0;
- padding: 12px;
- border-radius: var(--r-card);
+ padding: var(--s-3);
+ border-radius: var(--r-md);
color: var(--text);
text-align: left;
background: rgba(255, 255, 255, 0.028);
box-shadow: inset 0 0 0 1px var(--hairline);
- transition: background 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
+ transition: background var(--dur-fast) ease, box-shadow var(--dur-fast) ease, transform var(--dur-fast) ease;
}
.match-option:hover {
@@ -718,10 +720,10 @@
flex: 0 0 auto;
width: 28px;
height: 28px;
- border-radius: 999px;
+ border-radius: var(--r-pill);
color: var(--cyan-bright);
font-family: var(--font-mono);
- font-size: 12px;
+ font-size: var(--t-sm);
background: rgba(var(--cyan-rgb), 0.09);
box-shadow: inset 0 0 0 1px rgba(var(--cyan-rgb), 0.2);
}
@@ -729,32 +731,32 @@
.match-copy {
min-width: 0;
display: grid;
- gap: 3px;
+ gap: var(--s-05);
}
.match-copy strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- font-size: 13px;
- font-weight: 550;
+ font-size: var(--t-md);
+ font-weight: var(--w-medium);
}
.match-copy em {
color: var(--muted);
font-style: normal;
font-family: var(--font-mono);
- font-size: 9.5px;
- letter-spacing: 0.1em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-label);
text-transform: uppercase;
}
.match-hint {
- padding: 10px 18px 16px;
+ padding: var(--s-3) var(--s-5) var(--s-4);
color: var(--muted);
font-family: var(--font-mono);
- font-size: 9.5px;
- letter-spacing: 0.1em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-label);
}
/* ===== Onboarding / Settings panel ===== */
@@ -763,21 +765,19 @@
max-height: 88vh;
display: flex;
flex-direction: column;
- border-radius: 20px;
- animation: overlay-pop 0.24s cubic-bezier(0.2, 0.8, 0.2, 1);
+ border-radius: var(--r-xl);
+ animation: overlay-pop var(--dur-mid) var(--ease-out);
}
.setup-head {
display: flex;
align-items: center;
- gap: 12px;
- padding: 16px 18px;
- font-family: var(--font-mono);
- font-size: 10.5px;
- font-weight: 500;
- letter-spacing: 0.22em;
- text-transform: uppercase;
- color: var(--muted);
+ gap: var(--s-3);
+ padding: var(--s-4);
+ font-size: var(--t-base);
+ font-weight: var(--w-semi);
+ letter-spacing: var(--tr-snug);
+ color: var(--text);
border-bottom: 1px solid var(--hairline);
}
@@ -785,15 +785,15 @@
.setup-progress {
display: inline-flex;
- gap: 5px;
+ gap: var(--s-1);
}
.setup-progress i {
width: 18px;
height: 3px;
- border-radius: 999px;
+ border-radius: var(--r-pill);
background: rgba(148, 196, 255, 0.14);
- transition: background 0.3s ease, box-shadow 0.3s ease;
+ transition: background var(--dur-slow) ease, box-shadow var(--dur-slow) ease;
}
.setup-progress i.on {
@@ -805,54 +805,54 @@
flex: 1;
min-height: 0;
overflow-y: auto;
- padding: 18px;
+ padding: var(--s-5);
display: flex;
flex-direction: column;
- gap: 20px;
+ gap: var(--s-5);
}
.setup-section h3 {
- margin: 0 0 4px;
- font-size: 14.5px;
- font-weight: 600;
- letter-spacing: -0.005em;
+ margin: 0 0 var(--s-1);
+ font-size: var(--t-lg);
+ font-weight: var(--w-semi);
+ letter-spacing: var(--tr-snug);
color: var(--text);
}
.setup-hint {
- margin: 0 0 12px;
- font-size: 12px;
- line-height: 1.5;
+ margin: 0 0 var(--s-3);
+ font-size: var(--t-md);
+ line-height: var(--lh-body);
color: var(--muted);
}
.setup-field {
display: flex;
flex-direction: column;
- gap: 6px;
- margin-bottom: 13px;
+ gap: var(--s-2);
+ margin-bottom: var(--s-3);
}
.setup-field > span {
font-family: var(--font-mono);
- font-size: 9px;
- font-weight: 500;
- letter-spacing: 0.18em;
+ font-size: var(--t-2xs);
+ font-weight: var(--w-medium);
+ letter-spacing: var(--tr-caps);
text-transform: uppercase;
color: var(--muted);
}
.setup-field input {
width: 100%;
- padding: 10px 12px;
+ padding: var(--s-3);
border-radius: var(--r-sm);
- font-size: 13px;
+ font-size: var(--t-md);
font-family: var(--font-ui);
color: var(--text);
background: rgba(3, 8, 16, 0.55);
border: 1px solid var(--hairline);
outline: none;
- transition: border-color 0.15s ease, box-shadow 0.15s ease;
+ transition: border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}
.setup-field input::placeholder {
@@ -865,9 +865,9 @@
}
.setup-note {
- margin-top: 4px;
- font-size: 10.5px;
- line-height: 1.5;
+ margin-top: var(--s-1);
+ font-size: var(--t-xs);
+ line-height: var(--lh-body);
color: var(--muted);
}
@@ -883,13 +883,13 @@
.setup-note code {
font-family: var(--font-mono);
- font-size: 10px;
+ font-size: var(--t-2xs);
color: var(--text-soft);
}
.setup-inline {
display: flex;
- gap: 8px;
+ gap: var(--s-2);
}
/* --- Themed dropdown --- */
@@ -905,15 +905,15 @@
display: flex;
align-items: center;
justify-content: space-between;
- gap: 10px;
- padding: 10px 12px;
+ gap: var(--s-3);
+ padding: var(--s-3);
border-radius: var(--r-sm);
- font-size: 13px;
+ font-size: var(--t-md);
color: var(--text);
background: rgba(3, 8, 16, 0.55);
border: 1px solid var(--hairline);
cursor: pointer;
- transition: border-color 0.15s ease, box-shadow 0.15s ease;
+ transition: border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}
.ts-trigger:hover {
@@ -934,7 +934,7 @@
.ts-chev {
flex: 0 0 auto;
color: rgba(var(--cyan-rgb), 0.75);
- transition: transform 0.18s ease;
+ transition: transform var(--dur-fast) ease;
}
.ts-trigger.open .ts-chev {
@@ -945,11 +945,11 @@
z-index: 60;
max-height: 260px;
overflow-y: auto;
- padding: 6px;
- border-radius: 12px;
+ padding: var(--s-2);
+ border-radius: var(--r-md);
background: #0a1322;
border: 1px solid var(--hairline-strong);
- box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6);
+ box-shadow: var(--e-3);
animation: ts-menu-in 0.14s ease;
}
@@ -963,10 +963,10 @@
display: flex;
align-items: center;
justify-content: space-between;
- gap: 10px;
- padding: 9px 11px;
- border-radius: 8px;
- font-size: 13px;
+ gap: var(--s-3);
+ padding: var(--s-2) var(--s-3);
+ border-radius: var(--r-xs);
+ font-size: var(--t-md);
text-align: left;
color: var(--text-soft);
transition: background 0.12s ease, color 0.12s ease;
@@ -987,22 +987,26 @@
display: flex;
align-items: center;
flex-wrap: wrap;
- gap: 12px;
- margin: 2px 0 16px;
+ gap: var(--s-3);
+ margin: var(--s-05) 0 var(--s-4);
}
.setup-btn {
display: inline-flex;
align-items: center;
- gap: 6px;
- padding: 8px 15px;
- border-radius: 999px;
- font-size: 12.5px;
- font-weight: 550;
+ gap: var(--s-2);
+ padding: var(--s-2) var(--s-4);
+ border-radius: var(--r-pill);
+ font-size: var(--t-md);
+ font-weight: var(--w-medium);
color: var(--text);
background: rgba(255, 255, 255, 0.04);
box-shadow: inset 0 0 0 1px var(--hairline-strong);
- transition: transform 0.14s ease, background 0.14s ease, color 0.14s ease, box-shadow 0.14s ease;
+ transition:
+ transform var(--dur-fast) ease,
+ background var(--dur-fast) ease,
+ color var(--dur-fast) ease,
+ box-shadow var(--dur-fast) ease;
}
.setup-btn:hover:not(:disabled) {
@@ -1043,9 +1047,9 @@
.setup-result {
display: inline-flex;
align-items: center;
- gap: 5px;
- font-size: 11.5px;
- font-weight: 550;
+ gap: var(--s-1);
+ font-size: var(--t-sm);
+ font-weight: var(--w-medium);
}
.setup-result.ok { color: var(--mint); }
@@ -1059,8 +1063,8 @@
}
.setup-error {
- margin: -4px 0 8px;
- font-size: 11.5px;
+ margin: -4px 0 var(--s-2);
+ font-size: var(--t-sm);
color: var(--coral);
}
@@ -1068,41 +1072,41 @@
display: flex;
align-items: center;
justify-content: space-between;
- gap: 10px;
- padding: 14px 18px;
+ gap: var(--s-3);
+ padding: var(--s-4);
border-top: 1px solid var(--hairline);
}
.setup-foot-right {
display: flex;
align-items: center;
- gap: 10px;
+ gap: var(--s-3);
}
.setup-welcome h2 {
- margin: 4px 0 10px;
+ margin: var(--s-1) 0 var(--s-3);
font-family: var(--font-display);
- font-size: 22px;
- font-weight: 600;
- letter-spacing: -0.01em;
+ font-size: var(--t-2xl);
+ font-weight: var(--w-semi);
+ letter-spacing: var(--tr-tight);
color: var(--text);
}
.setup-welcome p {
margin: 0;
- font-size: 13.5px;
- line-height: 1.6;
+ font-size: var(--t-base);
+ line-height: var(--lh-loose);
color: var(--text-soft);
}
.setup-summary {
- margin: 16px 0 0;
+ margin: var(--s-4) 0 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
- gap: 7px;
- font-size: 13px;
+ gap: var(--s-2);
+ font-size: var(--t-md);
color: var(--text-soft);
}
@@ -1112,9 +1116,9 @@
}
.setup-path {
- margin: 4px 0 0;
+ margin: var(--s-1) 0 0;
font-family: var(--font-mono);
- font-size: 9.5px;
+ font-size: var(--t-2xs);
color: var(--muted);
word-break: break-all;
}
@@ -1123,9 +1127,9 @@
display: flex;
align-items: center;
justify-content: space-between;
- gap: 12px;
- padding: 9px 12px;
- margin-bottom: 8px;
+ gap: var(--s-3);
+ padding: var(--s-2) var(--s-3);
+ margin-bottom: var(--s-2);
border-radius: var(--r-sm);
background: rgba(255, 255, 255, 0.02);
box-shadow: inset 0 0 0 1px var(--hairline);
@@ -1133,30 +1137,30 @@
.setup-readonly span {
font-family: var(--font-mono);
- font-size: 9px;
- letter-spacing: 0.14em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-caps);
text-transform: uppercase;
color: var(--muted);
}
.setup-readonly code {
font-family: var(--font-mono);
- font-size: 11.5px;
+ font-size: var(--t-sm);
color: var(--text-soft);
}
.setup-perms {
display: flex;
flex-direction: column;
- gap: 10px;
+ gap: var(--s-3);
}
.setup-perm {
display: flex;
align-items: center;
- gap: 12px;
- padding: 12px;
- border-radius: var(--r-card);
+ gap: var(--s-3);
+ padding: var(--s-3);
+ border-radius: var(--r-md);
background: rgba(255, 255, 255, 0.028);
box-shadow: inset 0 0 0 1px var(--hairline);
}
@@ -1169,7 +1173,7 @@
place-items: center;
width: 34px;
height: 34px;
- border-radius: 9px;
+ border-radius: var(--r-xs);
color: var(--cyan-bright);
background: rgba(var(--cyan-rgb), 0.09);
}
@@ -1178,17 +1182,17 @@
flex: 1;
display: flex;
flex-direction: column;
- gap: 2px;
- font-size: 13px;
- font-weight: 550;
+ gap: var(--s-05);
+ font-size: var(--t-md);
+ font-weight: var(--w-medium);
color: var(--text);
}
.setup-perm .perm-label em {
font-style: normal;
font-family: var(--font-mono);
- font-size: 8.5px;
- letter-spacing: 0.16em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-caps);
text-transform: uppercase;
color: var(--muted);
}
@@ -1199,7 +1203,10 @@
animation: step-spin 0.7s linear infinite;
}
-/* ===== Boot sequence overlay ===== */
+/* ===== Boot sequence overlay =====
+ Cold start is the app's first impression, so it gets staged: the reactor
+ ignites, the wordmark resolves out of a blur with a light sweep across it,
+ then the log lines cascade in. */
.boot {
position: fixed;
inset: 0;
@@ -1208,10 +1215,12 @@
flex-direction: column;
align-items: center;
justify-content: center;
- gap: 18px;
+ gap: var(--s-4);
background:
- radial-gradient(52% 46% at 50% 40%, rgba(17, 34, 64, 0.55), transparent 72%),
- linear-gradient(180deg, var(--bg-1) 0%, var(--bg-0) 100%);
+ /* Flat, matching `.deck`. No glow behind the orb: a soft halo on a flat dark
+ field just reads as a faded blob, and the reactor art already carries the
+ light. */
+ var(--bg-1);
animation: boot-in 0.5s ease;
}
@@ -1219,47 +1228,70 @@
.boot .boot-orb {
width: min(34vh, 260px);
height: min(34vh, 260px);
+ animation: boot-orb-ignite 1.4s var(--ease-out) both;
}
-/* The orb ring/radar chrome are only shown inside .deck.awake; show them here too. */
+/* The orb ring/radar chrome are only shown inside .deck.awake; show them here too.
+ The plinth is deliberately NOT enabled: its blurred ellipse reads as a faded
+ shadow blob under the orb on an otherwise flat startup screen. */
.boot .orb-ring { opacity: 1; }
.boot .orb-radar { opacity: 0.7; }
+@keyframes boot-orb-ignite {
+ 0% { opacity: 0; transform: scale(0.82); filter: brightness(0.3) saturate(0.4); }
+ 55% { opacity: 1; filter: brightness(1.2) saturate(1.1); }
+ 100% { opacity: 1; transform: scale(1); filter: brightness(1) saturate(1); }
+}
+
.boot-title {
- margin-top: 2px;
+ position: relative;
+ margin-top: var(--s-05);
font-family: var(--font-display);
- font-weight: 600;
- font-size: 28px;
- letter-spacing: 0.5em;
- margin-right: -0.5em;
+ font-weight: var(--w-semi);
+ font-size: var(--t-3xl);
+ letter-spacing: var(--tr-brand);
+ margin-right: calc(var(--tr-brand) * -1);
color: #eaf6ff;
- text-shadow: 0 0 24px rgba(var(--cyan-rgb), 0.45);
+ text-shadow: 0 0 24px rgba(var(--accent-rgb), 0.5);
+ animation: boot-title-in 1s var(--ease-out) 0.25s both;
+}
+
+@keyframes boot-title-in {
+ from { opacity: 0; filter: blur(9px); letter-spacing: 0.75em; }
+ to { opacity: 1; filter: blur(0); letter-spacing: var(--tr-brand); }
}
.boot-sub {
font-family: var(--font-mono);
- font-size: 9px;
- letter-spacing: 0.52em;
- margin-right: -0.52em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-brand);
+ margin-right: calc(var(--tr-brand) * -1);
color: var(--muted);
text-transform: uppercase;
+ animation: boot-fade-up 0.7s var(--ease-out) 0.7s both;
}
.boot-log {
- margin-top: 8px;
+ margin-top: var(--s-2);
width: 320px;
font-family: var(--font-mono);
- font-size: 11.5px;
+ font-size: var(--t-xs);
+ animation: boot-fade-up 0.6s var(--ease-out) 0.95s both;
+}
+
+@keyframes boot-fade-up {
+ from { opacity: 0; transform: translateY(6px); }
+ to { opacity: 1; transform: translateY(0); }
}
.boot-line {
display: flex;
align-items: center;
- gap: 10px;
- padding: 3px 0;
+ gap: var(--s-3);
+ padding: var(--s-05) 0;
color: var(--muted);
- opacity: 0.5;
- transition: color 0.3s ease, opacity 0.3s ease;
+ opacity: 0.45;
+ transition: color var(--dur-slow) ease, opacity var(--dur-slow) ease;
}
.boot-line .boot-dot {
@@ -1272,8 +1304,8 @@
.boot-line .boot-state {
margin-left: auto;
- font-size: 9.5px;
- letter-spacing: 0.1em;
+ font-size: var(--t-2xs);
+ letter-spacing: var(--tr-label);
}
.boot-line.active {
@@ -1289,14 +1321,15 @@
.boot-bar {
width: 320px;
height: 2px;
- border-radius: 999px;
+ border-radius: var(--r-pill);
background: rgba(148, 196, 255, 0.12);
overflow: hidden;
+ animation: boot-fade-up 0.6s var(--ease-out) 1.05s both;
}
.boot-bar-fill {
height: 100%;
- border-radius: 999px;
+ border-radius: var(--r-pill);
background: linear-gradient(90deg, var(--cyan), var(--cyan-bright));
box-shadow: 0 0 12px rgba(var(--cyan-rgb), 0.6);
transition: width 0.35s ease;
@@ -1318,7 +1351,12 @@
@media (prefers-reduced-motion: reduce) {
.ts-menu,
- .spin {
+ .spin,
+ .boot .boot-orb,
+ .boot-title,
+ .boot-sub,
+ .boot-log,
+ .boot-bar {
animation: none;
}
}
diff --git a/src/styles/tokens.css b/src/styles/tokens.css
index f83bc64..09c85e6 100644
--- a/src/styles/tokens.css
+++ b/src/styles/tokens.css
@@ -1,24 +1,47 @@
/* =====================================================================
Iris design tokens — "Deep Space" system
Dark-only. Electric cyan primary, violet secondary, deep navy-black base.
- The orb (ReactorCore) keeps its own state palettes and is NOT themed here.
+
+ ONE LIGHT SOURCE: the reactor is the lamp of the room. `--orb-accent` is
+ published from React (App sets it on the deck / HUD root from the live
+ reactor state) and inherits down, so ambient glow, glass edge-light and
+ hairlines are all tinted by whatever Iris is doing right now. Semantic
+ colors (status, errors, text) never move — only the atmosphere does.
===================================================================== */
:root {
- /* Base space */
- --bg-0: #030509;
- --bg-1: #05070d;
- --bg-2: #0a0f1a;
+ /* ===== Base space =====
+ Deliberately NOT near-black. These used to be #030509/#05070d, i.e. code
+ values 3-13, and that is the single root cause of the contour lines this UI
+ had in its dim states. Two things go wrong that low:
+ - 8-bit has no resolution left, so a wide soft gradient has only a couple
+ of levels to cross and must render as huge flat plateaus with hard
+ 1-level edges between them.
+ - Below the sRGB toe, luminance is proportional to code value, so ONE code
+ step at value 7 is a ~14% relative brightness jump. At value 21 the same
+ step is ~5%, which is under the threshold where a long straight edge
+ reads as a line.
+ Keeping the darkest surface above ~#0b111c is what actually fixes banding;
+ no amount of noise or extra layers can rescue a 3-13 palette. Anything added
+ here later (vignettes, scrims, overlays) must respect the same floor, or it
+ drags its own region back into the crush zone. */
+ --bg-0: #0b111c;
+ --bg-1: #0e1522;
+ --bg-2: #151d2c;
+ /* Solid fallback for surfaces that must not be transparent. */
+ --bg: var(--bg-0);
- /* Accents */
+ /* ===== Accents ===== */
--cyan: #22d3ee;
--cyan-bright: #67e8f9;
+ --cyan-soft: #7dd3fc;
--cyan-rgb: 34, 211, 238;
--violet: #8b5cf6;
--violet-soft: #a78bfa;
+ --violet-bright: #c4b5fd;
--violet-rgb: 139, 92, 246;
- /* Status */
+ /* ===== Status ===== */
--amber: #f5b04a;
--amber-rgb: 245, 176, 74;
--mint: #2dd2af;
@@ -26,32 +49,171 @@
--coral: #f87171;
--coral-rgb: 248, 113, 113;
- /* Text */
+ /* ===== Text ===== */
--text: #e8f0fa;
--text-soft: #a9b9cd;
--muted: #64748e;
+ --text-dim: #4c5872;
+
+ /* ===== Reactive accent =====
+ Defaults to the reactor's "online" cyan; React publishes `--orb-accent`
+ per state on the room roots. Everything atmospheric consumes these, never
+ a raw color. See the re-declaration below `:root` — it is load-bearing. */
+ --accent-rgb: var(--orb-accent, 34, 190, 205);
+ --accent-glow: rgba(var(--accent-rgb), 0.5);
+ --accent-edge: rgba(var(--accent-rgb), 0.22);
+ --accent-wash: rgba(var(--accent-rgb), 0.06);
+ /* Slow enough that a state change reads as the room breathing, never a
+ strobe while you are working. */
+ --accent-ease: 1.2s cubic-bezier(0.4, 0, 0.2, 1);
- /* Glass surfaces */
- --panel: rgba(13, 20, 33, 0.52);
- --panel-strong: rgba(12, 19, 32, 0.86);
- --panel-solid: #0b1220;
+ /* ===== Glass surfaces =====
+ Fills are kept fairly opaque on purpose. `backdrop-filter` AVERAGES what is
+ behind it, which turns the ambient gradients into an even smoother ramp
+ inside the glass than outside it — the panels used to be the worst banding
+ in the app for exactly that reason. The more the fill covers, the smaller
+ the inherited ramp; once it spans less than one code level it is flat and
+ physically cannot band. Lowering these alphas re-introduces contour lines
+ across the panels. */
+ --panel: rgba(29, 40, 58, 0.78);
+ --panel-strong: rgba(26, 36, 53, 0.92);
+ --panel-solid: #1a2333;
+ /* Recessed rail: side panels sit BEHIND the orb in the visual hierarchy — but
+ still above `--bg-1`, or the glass stops reading as a surface at all now
+ that the base tone is no longer near-black. */
+ --panel-recessed: rgba(24, 33, 49, 0.74);
+ --panel-raised: rgba(255, 255, 255, 0.03);
--hairline: rgba(148, 196, 255, 0.09);
--hairline-strong: rgba(148, 210, 255, 0.16);
--edge-light: rgba(255, 255, 255, 0.05);
+ /* Real glass catches a brighter line along its top edge. */
+ --edge-bright: rgba(255, 255, 255, 0.1);
+
+ /* ===== HUD glass =====
+ The OS behind a transparent window cannot be backdrop-blurred, so
+ readable glass over an unknown wallpaper = a ~60% tinted fill, a bright
+ 1px edge to define the pane, and text shadows. One recipe, every island,
+ so the HUD reads as one system instead of scattered popups. */
+ --hud-fill: linear-gradient(180deg, rgba(26, 36, 57, 0.62), rgba(10, 16, 28, 0.66));
+ --hud-fill-strong: linear-gradient(180deg, rgba(26, 36, 57, 0.72), rgba(10, 16, 28, 0.76));
+ --hud-chip: rgba(22, 30, 47, 0.68);
+ --hud-edge: rgba(255, 255, 255, 0.18);
+ --hud-inner: inset 0 1px 0 rgba(255, 255, 255, 0.14);
+ --hud-lift: 0 1px 8px rgba(0, 0, 0, 0.2);
+ --hud-text: #f2f7ff;
+ --hud-text-soft: #c6d2e4;
+ --hud-text-dim: #a7b7cd;
+ --hud-shadow-text: 0 1px 3px rgba(0, 0, 0, 0.65);
+ --hud-shadow-text-soft: 0 1px 2px rgba(0, 0, 0, 0.55);
- /* Radii */
- --r-panel: 18px;
- --r-card: 14px;
+ /* ===== Radii ===== */
+ --r-xs: 6px;
--r-sm: 10px;
+ --r-md: 14px;
+ --r-lg: 18px;
+ --r-xl: 22px;
+ --r-pill: 999px;
+ /* Legacy aliases (still referenced widely). */
+ --r-panel: var(--r-lg);
+ --r-card: var(--r-md);
+
+ /* ===== Space (4px base) ===== */
+ --s-05: 2px;
+ --s-1: 4px;
+ --s-2: 8px;
+ --s-3: 12px;
+ --s-4: 16px;
+ --s-5: 20px;
+ --s-6: 24px;
+ --s-7: 32px;
+ --s-8: 40px;
+
+ /* ===== Type =====
+ Whole pixels only: half-pixel sizes never land on the hinting grid, which
+ is what made the old UI read permanently soft. Floor is 10px — below that
+ tracked-out uppercase mono stops being legible. */
+ --t-2xs: 10px;
+ --t-xs: 11px;
+ --t-sm: 12px;
+ --t-md: 13px;
+ --t-base: 14px;
+ --t-lg: 16px;
+ --t-xl: 20px;
+ --t-2xl: 24px;
+ --t-3xl: 30px;
+ /* Fluid: the orb caption and the wake prompt scale with the window. */
+ --t-caption: clamp(18px, 2.3vw, 26px);
+ --t-wake: clamp(15px, 1.8vw, 18px);
+
+ /* ===== Weight ===== */
+ --w-normal: 400;
+ --w-medium: 500;
+ --w-semi: 600;
+ --w-bold: 700;
- /* Type */
+ /* ===== Tracking =====
+ The instrument voice is now RATIONED. Tracked uppercase mono is reserved
+ for genuine readouts (telemetry, run ids, boot log, status); panel
+ headers and chips speak in the UI font so hierarchy survives. */
+ --tr-tight: -0.01em;
+ --tr-snug: -0.005em;
+ --tr-label: 0.08em;
+ --tr-caps: 0.14em;
+ --tr-wide: 0.2em;
+ --tr-brand: 0.5em;
+
+ /* ===== Line height ===== */
+ --lh-tight: 1.25;
+ --lh-snug: 1.4;
+ --lh-body: 1.55;
+ --lh-loose: 1.7;
+
+ /* ===== Font families ===== */
--font-ui: "Inter Variable", "Inter", system-ui, sans-serif;
--font-display: "Space Grotesk Variable", "Space Grotesk", var(--font-ui);
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
- /* Shadows */
+ /* ===== Elevation =====
+ Four altitudes. Panels get an accent-tinted bloom instead of a pure
+ black drop so surfaces feel lit by the reactor rather than stamped on. */
+ --e-1:
+ inset 0 1px 0 var(--edge-light),
+ 0 2px 8px rgba(0, 0, 0, 0.25);
+ --e-2:
+ inset 0 1px 0 var(--edge-bright),
+ 0 16px 40px rgba(0, 0, 0, 0.42),
+ 0 0 60px -20px rgba(var(--accent-rgb), 0.16);
+ --e-3:
+ inset 0 1px 0 var(--edge-bright),
+ 0 24px 64px rgba(0, 0, 0, 0.55);
+ --e-4:
+ inset 0 1px 0 var(--edge-bright),
+ 0 40px 110px rgba(0, 0, 0, 0.66);
+ /* Legacy aliases. */
--shadow-panel: 0 24px 64px rgba(0, 0, 0, 0.5);
--shadow-float: 0 40px 110px rgba(0, 0, 0, 0.66);
+ /* ===== Motion ===== */
+ --ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);
+ --dur-fast: 0.15s;
+ --dur-mid: 0.24s;
+ --dur-slow: 0.4s;
+
color-scheme: dark;
}
+
+/* Re-resolve the accent on each "room root".
+ Custom properties substitute ONCE, on the element carrying the declaration,
+ and then inherit as a fixed computed value. So the `--accent-rgb` above
+ resolves against `:root`, where `--orb-accent` does not exist yet — it locks
+ in the fallback and inheriting it downward would keep the whole UI on default
+ cyan forever, no matter what React publishes.
+ Re-declaring it here forces a fresh substitution on the elements that
+ actually carry `--orb-accent`, so the reactive accent genuinely tracks the
+ reactor. Do not fold this back into `:root`. */
+.deck,
+.hud-shell,
+.boot,
+.orb-stage {
+ --accent-rgb: var(--orb-accent, 34, 190, 205);
+}