@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 { color-scheme: dark; --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: #ee928c; --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; /* Markdown heading weight (appearance settings; 900 = the Black cut). */ --lh-heading-weight: 900; } /* The app is always 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: #ee928c; --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; /* Markdown heading weight (appearance settings; 900 = the Black cut). */ --lh-heading-weight: 900; } /* 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-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.) */ /* --font-title lives in the NON-inline @theme below: inline tokens get their literal values baked into the utilities (.font-title{font-family: lh-cmu,serif}), which made the appearance "session names in serif" toggle write a runtime --font-title no consumer ever read (owner, fix wave: toggle looked dead). Non-inline emits :root { --font-title: … } and the utility reads var(--font-title) — appearance overrides win. */ --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); } /* Session-name weight (owner, fix wave round 3): with the serif toggle OFF, --font-title resolves to the prose font — Source Sans 3 is loaded as a VARIABLE font, and without an explicit weight the titles render at 400, a whisper next to CMU's bold-only face. Request 900: the variable font takes it, and CMU (a single 700 face) snaps to its nearest available weight — the same rendering it already had. */ .font-title { font-weight: 900; } .lh-prose :where(strong, b) { color: var(--lh-ink-strong); font-weight: 700; } .lh-prose :where(em, i) { color: var(--lh-ink-italic); } /* Markdown lists indent from the prose edge (owner ask, 2026-09-24): streamdown ships list-inside with no container padding (top-level markers sit ON the text edge — nested lists get their pl-6 only via [li_&]). 24px — the same measure as that nested-case pl-6 — pulls the whole list right as a block. Unlayered CSS beats the utility layer; :where keeps specificity zero. */ .lh-prose :where(ul, ol) { padding-left: 1.5rem; } /* Markdown headings render at the font's BLACK weight (owner ask, 2026-09-19) — 900, not streamdown's font-semibold 600. The variable Source Sans 3 axis spans 200–900 (next/font loads the full range), so 900 draws the real Black cut. Unlayered CSS beats the utility layer; :where keeps specificity zero. Chrome titles (CMU Serif) don't live under .lh-prose and stay untouched. */ .lh-prose :where(h1, h2, h3, h4, h5, h6) { font-weight: var(--lh-heading-weight, 900); } /* 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; } /* ============================================================ * Chrome melt (owner wave, 2026-09-19) — tables and code blocks stop * being boxes: blocks read as part of the message. These rules override * streamdown's Tailwind utilities (utilities live in a cascade LAYER; * unlayered author CSS wins regardless of order). * ============================================================ */ /* Code blocks: no header bar — one quiet rounded tint so code reads as a unit. The tint is DARKER than the page (muted #1c1c1c was LIGHTER than the background #141414 and read as raised — owner note, chrome-melt tweaks 2026-09-19) plus a subtle hairline outline. (md-blocks renders its own hover copy corner.) */ [data-streamdown="code-block"] { border: 1px solid var(--border); background: color-mix(in oklab, var(--background) 86%, black); border-radius: 0.75rem; padding: 0.375rem; } [data-streamdown="code-block-header"] { display: none; } [data-streamdown="code-block-body"] { border: 0; background: transparent; padding: 0.75rem 1rem; } /* Tables: hairline row rules + header underline, and a full outer border — actual RECTANGLES outlined on all four sides (owner note, chrome-melt tweaks 2026-09-19: the melt left the shape missing its left/right/top edges). border-collapse merges the outer border with the row rules, so no edge doubles. (md-blocks supplies the wrapper + the copy corner.) */ [data-streamdown="table"] { border: 1px solid var(--border); /* Tables run at the SESSION prose size (owner note, 2026-09-19: they visibly lagged the 16px chat text at streamdown's 12.5px text-sm). Unlayered CSS beats the cells' own utility-layer text-sm, and the :where keeps specificity zero — the override wins on layering alone. */ font-size: var(--lh-prose-size); } [data-streamdown="table"] :where(th, td) { font-size: inherit; } /* Column separators: a hairline left rule on every cell but the first in its row (owner ask, 2026-09-19) — border-collapse merges it with the row rules and the outer border, no doubled lines anywhere. */ [data-streamdown="table"] :where(th, td):not(:first-child) { border-left: 1px solid var(--border); } [data-streamdown="table-header"] { background: transparent; border-bottom: 1px solid var(--border); } [data-streamdown="table-body"] [data-streamdown="table-row"] { border-bottom: 1px solid var(--border); } /* The mermaid focus overlay's chart wrapper: the svg fills the wrapper (which is sized to the chart's viewBox — pan-zoom.ts math centers on it), overriding mermaid's inline max-width cap. Later + same-specificity than the column rule above, so the !important tie breaks our way; the overlay's svg never sits under [data-streamdown="mermaid"] anyway. */ [data-lh-overlay-chart] svg { max-width: none !important; width: 100% !important; height: auto !important; } /* ============================================================ * 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 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; } } /* Settings dialog pop (settings-app wave, 2026-09-20): a short fade + 2% settle-scale — understated by design, the app's motion taste (200ms sidebar, 150ms here); honors reduced-motion by appearing. */ @keyframes settings-pop { from { opacity: 0; scale: 0.98; } to { opacity: 1; scale: 1; } } .settings-dialog-pop { animation: settings-pop 150ms ease-out; } @media (prefers-reduced-motion: reduce) { .settings-dialog-pop { 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 , which SidebarProvider sets after mount when it detects the Electron shell. - .lh-drag window-drag region — on a dedicated mask div in home.tsx placed FIRST in the page DOM (the mask resolves by DOM order, not z-index: no-drag subtracts only regions added earlier in the tree) - .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-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; } /* --font-title deliberately NOT @theme inline (see comment above): the session-name-serif toggle overrides this var at runtime, so the .font-title utility must read it through var(), not carry a baked copy. */ @theme { --font-title: "lh-cmu", serif; }