Files
learnharness/app/globals.css
T
amosblomqvist 6b58d87d9e electron+tests: zoom plumbing (construction-only lights), hermeticity fence for the jobs suite
- ZOOM: the ⌘±/⌘0 ladder is owned, not stock roles — applyZoom sets the
  factor and broadcasts 'lh:zoom'; the renderer publishes --lh-zoom; the
  gutter lengths (.lh-lights-gutter / .lh-closed-toggle) divide by it so
  their RENDERED size is a physical constant at every zoom — the graph
  button stays glued next to the lights, nothing slides under them.
  before-input-event catches ⌘=/layout variants the menu's Plus may not
  claim; did-finish-load re-pushes on reload; getZoom covers first paint.
  The lights themselves stay at construction position — Electron 44 has
  NO setTrafficLightPosition (options-only; the first run crashed on
  it). HIG-acceptable: native lights hug the corner; the web chrome
  compensates around them.
- JOBS SUITE FLAKE, actually fixed this time: the seams are
  module-global and the sweep scans every record — a timed-out async
  test left a running orphan + late wake that poisoned its NEIGHBORS
  (respawned 3 !== 0). New afterEach fence: settle every tracked
  running job, unregister aborts, seams reset to inert no-ops — every
  test starts from identical state regardless of upstream flake.
  Verified: two consecutive full-suite runs 1326/1326.
2026-10-05 05:42:27 +03:00

568 lines
21 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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).
*/
/* The no-ring law (owner, UI round 2026-10-01): the browser's default
focus/focus-visible outline appears NOWHERE in the app. Pointer focus
never showed it; any keypress while a clicked element still held focus
DID (:focus-visible re-evaluates on key input — a clicked sidebar
session lit up when the quiz's ←/→ were pressed). Surfaces carry their
own hover/active/selected affordances; the dock's keyboard walk uses
its own ring utilities, unaffected by an outline reset. */
*:focus,
*:focus-visible {
outline: none;
}
/* 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 inks default to the REGULAR TEXT COLOR (owner ruling,
appearance wave 2026-09-26 — the old #ee928c/#6fa9d6 factory
emphasis colors are gone). Explicit overrides land via the
appearance settings vars. */
--lh-ink-strong: var(--foreground);
--lh-ink-italic: var(--foreground);
/* 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 (<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;
/* Same emphasis ruling as the block above: unset = text color. */
--lh-ink-strong: var(--foreground);
--lh-ink-italic: var(--foreground);
/* 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 120ms 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 <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;
}
}
/* 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;
}
}
/* Graph chrome fade-in (empty-graph chrome wave, owner 2026-09-25): the
search + settings controls appear ONCE, as the first nodes land —
Import Notes always renders (the empty graph's entry point), the other
two mount only when the snapshot has nodes. Same understated register
as settings-pop (a plain fade); reduced-motion appears instantly. */
@keyframes chrome-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.graph-chrome-fade-in {
animation: chrome-fade-in 200ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.graph-chrome-fade-in {
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 — 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
(native interactive elements carve out AUTOMATICALLY —
see the auto carve-out rule below; manual use is for
clickable non-natives only)
- .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;
}
/* THE AUTO CARVE-OUT (owner standardization ruling, 2026-09-29 night):
inside a drag band, every NATIVE interactive element is automatically
a no-drag island — "draggable anywhere that is not a clickable
element" becomes structural, not per-child discipline. TopBar
contents can differ arbitrarily; buttons/inputs/links always stay
clickable with zero opt-out bookkeeping. Manual .lh-no-drag remains
only for clickable NON-native elements (spans/divs with onClick —
e.g. the graph chrome's control column). */
.lh-electron .lh-drag :is(button, input, textarea, select, a, summary, [contenteditable]) {
-webkit-app-region: no-drag;
}
/* 92px = measured lights' right edge (~75pt) + the lights' own 16px
inset from the window edge — comfortable, symmetric air (owner).
PHYSICAL-CONSTANT under zoom (owner round 7, 2026-10-05): ⌘±/⌘0 scale
every CSS px by the zoomFactor while the native lights never move —
so these lengths DIVIDE by --lh-zoom (published by the renderer from
the lh:zoom IPC) to render at the same physical size at every zoom.
The graph button stays glued next to the lights zoomed in; nothing
slides under them zoomed out. Default 1 until the first push lands. */
.lh-electron {
--lh-zoom: 1;
}
.lh-electron .lh-lights-gutter {
padding-left: calc(92px / var(--lh-zoom));
}
.lh-electron .lh-closed-toggle {
margin-left: calc(92px / var(--lh-zoom));
}
/* --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;
}
/* Card enter (owner round, 2026-10-05): the transcript cards' one-shot
mount — a subtle fade + 4px rise, the app's motion register (150ms,
dock-variant/settings-pop taste). Nothing on update, nothing on
unmount. Applied by the unified ThinkingBlock and the practice
cards; honors reduced-motion by appearing. */
@keyframes lh-card-enter {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.lh-card-enter {
animation: lh-card-enter 120ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.lh-card-enter {
animation: none;
}
}