The system title bar disappears (titleBarStyle hiddenInset +
trafficLightPosition {16,24}): the app's own 60px header bands become the
window's drag surface, and the traffic lights float over the sidebar's
mark band, vertically centered on the same line as the chat header's
phase pill (pixel-verified: circle centers y 24–36.5pt, pill box center
30pt).
- electron/main.cjs: hiddenInset chrome; dev shell (LH_DEV_URL) gets its
own userData (-dev suffix) so it never fights the packaged app's
single-instance lock — verified: the lock silently killed the dev
shell while the packaged app was open (that also proved the lock works
across separately-launched builds).
- app/globals.css: .lh-electron-gated utilities — drag/no-drag bands,
92px traffic-light gutter (.lh-lights-gutter / .lh-closed-toggle:
lights end ~75pt + the lights' own 16px edge inset as the gap),
home's floating strip becomes a drag surface only in the shell.
Everything is inert in the plain browser.
- sidebar.tsx: SidebarProvider sets <html class=lh-electron> post-mount
(UA detection; no hydration mismatch, absent in browser); LH wordmark
text replaced by the owner's mark (components/lh-mark.tsx, inlined
icon-no-padding.svg, 24px, home link kept); SidebarToggle gains the
92px margin when closed so it never slides under the static lights.
- chat.tsx: the rename hit-box shrinks to the title text itself —
flex-1 spanned the whole bar, so every click started a rename and the
window lost its grip. Shrink-to-fit + a flex-1 drag spacer (chips stay
right) + hover:bg-accent affordance (clear when hover renames vs
drags; -mx/px cancel for zero shift). The renaming input keeps flex-1
for edit room.
- home.tsx / new-session.tsx / phase-divider.tsx: drag bands + carve-outs.
Live-verified in the dev shell (pixel analysis) and headless browser:
lights/pill/mark centered, 17pt lights→mark gap, rename click→select-all
→Escape roundtrip, browser layout untouched.
Gates: typecheck, eslint, 500/500 tests, build.
344 lines
12 KiB
CSS
344 lines
12 KiB
CSS
@import "tailwindcss";
|
||
|
||
@custom-variant dark (&:is(.dark *));
|
||
|
||
/*
|
||
* THE SKIN — wave 5. Design tokens, the single place to restyle the app.
|
||
*
|
||
* Mathematical soundness, per the owner's brief:
|
||
* - Tailwind's built-in 0.25rem (4px) spacing scale IS the rhythm — every
|
||
* padding/margin/gap in the app uses whole units of a 4px base.
|
||
* - The type scale below is a declared modular ladder (each --text-* token
|
||
* carries its own line-height); components never invent pixel sizes.
|
||
* - Radii ladder is 4 / 8 / 12 / 16px multiples of the 4px base.
|
||
* - Emphasis colors live in tokens (--lh-ink-strong / --lh-ink-italic) so a
|
||
* future settings UI reassigns them (configurable per the owner).
|
||
*/
|
||
|
||
/* Palette (owner's hexes): main #141414, raised #1C1C1C, edges #434343.
|
||
Mono chrome = JetBrains Mono; prose = Source Sans 3; titles = CMU Serif. */
|
||
:root {
|
||
--radius: 8px;
|
||
--background: #141414;
|
||
--foreground: #e6e6e6;
|
||
--card: #1c1c1c;
|
||
--card-foreground: #e6e6e6;
|
||
--popover: #1c1c1c;
|
||
--popover-foreground: #e6e6e6;
|
||
--primary: #e6e6e6;
|
||
--primary-foreground: #141414;
|
||
--secondary: #1c1c1c;
|
||
--secondary-foreground: #e6e6e6;
|
||
--muted: #1c1c1c;
|
||
--muted-foreground: #8d8d8d;
|
||
--accent: #2a2a2a;
|
||
--accent-foreground: #e6e6e6;
|
||
--destructive: oklch(0.704 0.191 22.216);
|
||
--border: #434343;
|
||
--input: #434343;
|
||
--ring: #8d8d8d;
|
||
/* Emphasis coloring in teaching prose — ready for the settings UI. */
|
||
--lh-ink-strong: #c96b4a;
|
||
--lh-ink-italic: #6fa9d6;
|
||
/* THE READING SIZE = the ladder's 16px rung (--text-lg). Can't be
|
||
var()-linked: @theme inline inlines literals into utilities without
|
||
emitting runtime vars — so this is the one documented duplication:
|
||
change one, change both. (owner, typography round 4: snapped from
|
||
the reskin's hand-tuned 16.5 to the ladder's 16.) */
|
||
--lh-prose-size: 16px;
|
||
--lh-prose-leading: 1.5;
|
||
}
|
||
|
||
/* The app is always dark (<html class="dark">); mirror the same values so
|
||
the block stays coherent under Tailwind's variant. */
|
||
.dark {
|
||
--background: #141414;
|
||
--foreground: #e6e6e6;
|
||
--card: #1c1c1c;
|
||
--card-foreground: #e6e6e6;
|
||
--popover: #1c1c1c;
|
||
--popover-foreground: #e6e6e6;
|
||
--primary: #e6e6e6;
|
||
--primary-foreground: #141414;
|
||
--secondary: #1c1c1c;
|
||
--secondary-foreground: #e6e6e6;
|
||
--muted: #1c1c1c;
|
||
--muted-foreground: #8d8d8d;
|
||
--accent: #2a2a2a;
|
||
--accent-foreground: #e6e6e6;
|
||
--destructive: oklch(0.704 0.191 22.216);
|
||
--border: #434343;
|
||
--input: #434343;
|
||
--ring: #8d8d8d;
|
||
--lh-ink-strong: #c96b4a;
|
||
--lh-ink-italic: #6fa9d6;
|
||
/* THE READING SIZE = the ladder's 16px rung (--text-lg). Can't be
|
||
var()-linked: @theme inline inlines literals into utilities without
|
||
emitting runtime vars — so this is the one documented duplication:
|
||
change one, change both. (owner, typography round 4: snapped from
|
||
the reskin's hand-tuned 16.5 to the ladder's 16.) */
|
||
--lh-prose-size: 16px;
|
||
--lh-prose-leading: 1.5;
|
||
}
|
||
|
||
/* CMU Serif bold (titles) — bundled local file, no CDN dependency. */
|
||
@font-face {
|
||
font-family: "lh-cmu";
|
||
src: url("/fonts/cmunbx.ttf") format("truetype");
|
||
font-weight: 700;
|
||
font-style: normal;
|
||
font-display: swap;
|
||
}
|
||
|
||
@theme inline {
|
||
/* Fonts: chrome = JetBrains Mono, prose = Source Sans 3, titles = CMU
|
||
Serif bold. The JetBrains/SourceSans variables attach in app/layout.tsx.
|
||
(owner, typography round) The Tailwind defaults are ALIASED to the app
|
||
fonts: `font-mono`/`font-sans` utilities would otherwise fall to the
|
||
SYSTEM stacks — a fourth font the app never chose — and streamdown's
|
||
code-block chrome + mermaid-error + demo-card all use them. */
|
||
--font-ui: var(--font-jetbrains), ui-monospace, SFMono-Regular, monospace;
|
||
--font-prose: var(--font-sans3), ui-sans-serif, system-ui, sans-serif;
|
||
--font-title: "lh-cmu", serif;
|
||
--font-mono: var(--font-jetbrains), ui-monospace, SFMono-Regular, monospace;
|
||
--font-sans: var(--font-sans3), ui-sans-serif, system-ui, sans-serif;
|
||
/* The modular type ladder — each size carries its own line-height.
|
||
Component sizes must pull from this ladder. (owner, typography
|
||
round 4: the 10px rung is merged into 11 — one pixel apart was a
|
||
distinction with no visible difference, and 10px sits at the mono
|
||
legibility floor; quiet labels get their quiet from color +
|
||
tracking, not a notch of size. Reading prose = the 16px rung via
|
||
--lh-prose-size.) */
|
||
--text-xs: 11px;
|
||
--text-xs--line-height: 1.4;
|
||
--text-sm: 12.5px;
|
||
--text-sm--line-height: 1.4;
|
||
--text-base: 14px;
|
||
--text-base--line-height: 1.45;
|
||
--text-lg: 16px;
|
||
--text-lg--line-height: 1.45;
|
||
--text-xl: 20px;
|
||
--text-xl--line-height: 1.4;
|
||
--text-2xl: 25px;
|
||
--text-2xl--line-height: 1.3;
|
||
--text-3xl: 32px;
|
||
--text-3xl--line-height: 1.25;
|
||
/* Radii ladder — 4/8/12/16 multiples of the 4px base. */
|
||
--radius-sm: 4px;
|
||
--radius-md: 8px;
|
||
--radius-lg: 12px;
|
||
--radius-xl: 16px;
|
||
--radius: 8px;
|
||
--color-background: var(--background);
|
||
--color-foreground: var(--foreground);
|
||
--color-card: var(--card);
|
||
--color-card-foreground: var(--card-foreground);
|
||
--color-popover: var(--popover);
|
||
--color-popover-foreground: var(--popover-foreground);
|
||
--color-primary: var(--primary);
|
||
--color-primary-foreground: var(--primary-foreground);
|
||
--color-secondary: var(--secondary);
|
||
--color-secondary-foreground: var(--secondary-foreground);
|
||
--color-muted: var(--muted);
|
||
--color-muted-foreground: var(--muted-foreground);
|
||
--color-accent: var(--accent);
|
||
--color-accent-foreground: var(--accent-foreground);
|
||
--color-destructive: var(--destructive);
|
||
--color-border: var(--border);
|
||
--color-input: var(--input);
|
||
--color-ring: var(--ring);
|
||
}
|
||
|
||
* {
|
||
border-color: var(--color-border);
|
||
}
|
||
|
||
body {
|
||
background: var(--color-background);
|
||
color: var(--color-foreground);
|
||
font-family: var(--font-ui);
|
||
}
|
||
|
||
/* Teaching prose (the markdown renderer's container gets .lh-prose):
|
||
Source Sans 3 with the size/leading tokens; emphasis color bound to
|
||
the settings-ready tokens. */
|
||
.lh-prose {
|
||
font-family: var(--font-prose);
|
||
font-size: var(--lh-prose-size);
|
||
line-height: var(--lh-prose-leading);
|
||
}
|
||
|
||
.lh-prose :where(strong, b) {
|
||
color: var(--lh-ink-strong);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.lh-prose :where(em, i) {
|
||
color: var(--lh-ink-italic);
|
||
}
|
||
|
||
/* lh-input (owner, typography round 2): lh-prose's 1.5 READING leading
|
||
makes typing carets tower over the glyphs — the caret fills the line
|
||
box (24px at 16px text, ~2× the glyph height). Inputs are not
|
||
paragraphs: a tighter 1.25 leading brings the caret to the font-metric
|
||
floor (~20px, as close to the glyphs as Chromium will draw it) while
|
||
wrapped lines in growing textareas still read fine. Rendered prose keeps
|
||
1.5 — the two share family and size, they differ only in leading. */
|
||
.lh-input {
|
||
font-family: var(--font-prose);
|
||
font-size: var(--lh-prose-size);
|
||
line-height: 1.25;
|
||
}
|
||
|
||
/* Thin dark native scrollbar (app-wide): dark thumb, transparent track. */
|
||
* {
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--color-border) transparent;
|
||
}
|
||
|
||
::-webkit-scrollbar {
|
||
width: 8px;
|
||
height: 8px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: var(--color-border);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: var(--color-muted-foreground);
|
||
}
|
||
|
||
/* Dock variant swap — quick fade/rise on content change; the shell's HEIGHT
|
||
morph is separate (WAAPI in DockOverlay). Re-applied via class toggling, no
|
||
remount, so collapsed-question state survives. */
|
||
@keyframes dock-variant-in {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(6px);
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.dock-variant {
|
||
animation: dock-variant-in 160ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||
}
|
||
|
||
/* ============================================================
|
||
* W3 — presentation fixes (appended 2026-08-22)
|
||
* ============================================================ */
|
||
|
||
/* Streamdown's internal UI chrome (code blocks, tables, mermaid containers,
|
||
copy/download buttons) is styled with Tailwind utilities living in its dist
|
||
* bundle. Tailwind v4 never scans node_modules, so without this line those
|
||
* classes are dead — code blocks and mermaid containers rendered unstyled.
|
||
* (Streamdown README, "Tailwind CSS setup".) */
|
||
@source "../node_modules/streamdown/dist/*.js";
|
||
|
||
/* Token the streamdown utilities reference (code-block/mermaid chrome uses
|
||
* bg-sidebar / border-sidebar): map it onto the app's card surface. */
|
||
@theme inline {
|
||
--color-sidebar: var(--card);
|
||
}
|
||
|
||
/* Mermaid diagrams: keep wide charts inside the content column. Mermaid
|
||
* inlines a px max-width on the svg, so !important is required to win. */
|
||
[data-streamdown="mermaid"] svg {
|
||
max-width: 100% !important;
|
||
height: auto !important;
|
||
}
|
||
|
||
/* Display math: long equations scroll instead of overflowing the column. */
|
||
.katex-display {
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
}
|
||
|
||
/* ============================================================
|
||
* Session status — the braille spinner (the one working indicator)
|
||
* ============================================================ */
|
||
|
||
/* (owner, minimal-color round) Phase color-coding is RETIRED: phases only
|
||
advance (every session ends in Teaching), so phase-as-signal was
|
||
decoration; the --phase-* tokens and every phase-dot state are gone.
|
||
What state deserved — "a turn is in flight" — is carried by MOTION
|
||
alone: the terminal braille spinner (components/braille-spinner.tsx),
|
||
monochrome, existing only while true. */
|
||
|
||
/* One keyframes, all ten frames: each frame <g> holds opacity 1 for its
|
||
10% slice of the 800ms cycle and 0 elsewhere; frames stagger via
|
||
positive animation-delay (frame k starts at k×80ms). Static contexts
|
||
(SSR, reduced motion) show frame 0 — see the base rules below. */
|
||
@keyframes braille-frame {
|
||
0%,
|
||
10% {
|
||
opacity: 1;
|
||
}
|
||
|
||
10.001%,
|
||
100% {
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
.braille-frame {
|
||
animation: braille-frame 800ms linear infinite;
|
||
opacity: 0;
|
||
}
|
||
|
||
/* Static fallback: frame 0 (⠋) is the glyph a non-animated context shows. */
|
||
.braille-frame[data-frame="0"] {
|
||
opacity: 1;
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.braille-frame {
|
||
animation: none;
|
||
}
|
||
}
|
||
|
||
/* ─────────────────────────────────────────────────────────────────────────
|
||
Electron merged chrome (owner, 2026-09-17). The system title bar is
|
||
hidden (titleBarStyle: "hiddenInset" in electron/main.cjs); the app's
|
||
own 60px header bands are the drag surface. Everything here is inert
|
||
in the plain browser — it keys off <html class="lh-electron">, which
|
||
SidebarProvider sets after mount when it detects the Electron shell.
|
||
|
||
- .lh-drag window-drag region (must sit on pointer-receiving els)
|
||
- .lh-no-drag carve-out for interactive elements inside a drag band
|
||
- .lh-lights-gutter the sidebar LH band's traffic-light gutter: lights
|
||
end at x≈68, the wordmark starts at 80px (5rem)
|
||
- .lh-closed-toggle same gutter for the sidebar toggle when the sidebar
|
||
is closed — the lights are static, the toggle moves
|
||
- .lh-float-drag the home header strip floats pointer-events-none over
|
||
the graph; only in Electron does it claim events and
|
||
become the drag surface for that page
|
||
───────────────────────────────────────────────────────────────────────── */
|
||
.lh-electron .lh-drag {
|
||
-webkit-app-region: drag;
|
||
}
|
||
.lh-electron .lh-no-drag {
|
||
-webkit-app-region: no-drag;
|
||
}
|
||
/* 92px = measured lights' right edge (~75pt) + the lights' own 16px
|
||
inset from the window edge — comfortable, symmetric air (owner). */
|
||
.lh-electron .lh-lights-gutter {
|
||
padding-left: 92px;
|
||
}
|
||
.lh-electron .lh-closed-toggle {
|
||
margin-left: 92px;
|
||
}
|
||
.lh-electron .lh-float-drag {
|
||
pointer-events: auto;
|
||
-webkit-app-region: drag;
|
||
}
|