// Lab-Notebook design tokens → CSS. claude-code-hub design system, verbatim // tokens. Dark-first (#101114) + warm-paper light (#e8e6e3) siblings; one ember // accent; tonal-first depth, flat at rest; two-voice serif/mono type. import { KIT_BG } from "./kit.ts"; /** The 12 color variables a color theme must define, per mode. */ export interface Palette { ember: string; emberGlow: string; emberDim: string; field: string; surface: string; elevated: string; hover: string; border: string; ink1: string; ink2: string; ink3: string; inkMuted: string; } export interface ColorTheme { id: string; label: string; dark: Palette; light: Palette; } export const DEFAULT_COLOR_THEME = "ember"; // Curated registry. "ember" mirrors the :root defaults below (it has no override // block — clearing data-color-theme is how you get it); the ports map canonical // upstream palettes onto the Lab-Notebook variable roles: one accent (--ember*), // four surfaces (field→hover), one hairline (border), four-step ink ramp. // Dark-only upstreams get a hand-derived light sibling (see per-theme comments); // emberDim is mechanical: rgba(ember, 0.25) dark / 0.18 light. export const COLOR_THEMES: ColorTheme[] = [ { id: "ember", label: "Ember", dark: { ember: "#e86f33", emberGlow: "#f0a070", emberDim: "rgba(232,111,51,0.25)", field: "#101114", surface: "#16181c", elevated: "#1e2025", hover: "#282a30", border: "#363840", ink1: "#f0f1f3", ink2: "#c2c4c9", ink3: "#9a9da5", inkMuted: "#7d808a", }, light: { ember: "#e86f33", emberGlow: "#a0501c", emberDim: "rgba(184,90,32,0.18)", field: "#e8e6e3", surface: "#efede9", elevated: "#fbfaf9", hover: "#e0ddd7", border: "#cfcbc4", ink1: "#0a0a0a", ink2: "#2c2c2c", ink3: "#565656", inkMuted: "#767676", }, }, { id: "gruvbox", label: "Gruvbox", dark: { ember: "#fe8019", emberGlow: "#f9b27c", emberDim: "rgba(254,128,25,0.25)", field: "#1d2021", surface: "#282828", elevated: "#32302f", hover: "#3c3836", border: "#504945", ink1: "#ebdbb2", ink2: "#d5c4a1", ink3: "#bdae93", inkMuted: "#928374", }, // Light surfaces are deliberately desaturated vs canonical gruvbox-light // (#f2e5bc/#f9f5d7): the full-strength yellow wash was hard to read on. Keep // the warm cast + gruvbox inks/accent, drop the saturation of the paper. light: { ember: "#d65d0e", emberGlow: "#af3a03", emberDim: "rgba(214,93,14,0.18)", field: "#ede7d5", surface: "#f3eee0", elevated: "#faf7ec", hover: "#e4dcc6", border: "#c9bc9d", ink1: "#3c3836", ink2: "#504945", ink3: "#665c54", inkMuted: "#7c6f64", }, }, { id: "catppuccin", label: "Catppuccin", dark: { ember: "#cba6f7", emberGlow: "#d8c2fa", emberDim: "rgba(203,166,247,0.25)", field: "#11111b", surface: "#181825", elevated: "#1e1e2e", hover: "#313244", border: "#45475a", ink1: "#cdd6f4", ink2: "#bac2de", ink3: "#a6adc8", inkMuted: "#7f849c", }, light: { ember: "#8839ef", emberGlow: "#6f2dbd", emberDim: "rgba(136,57,239,0.18)", field: "#e6e9ef", surface: "#eff1f5", elevated: "#ffffff", hover: "#ccd0da", border: "#bcc0cc", ink1: "#4c4f69", ink2: "#5c5f77", ink3: "#6c6f85", inkMuted: "#8c8fa1", }, }, { id: "tokyo-night", label: "Tokyo Night", dark: { ember: "#7aa2f7", emberGlow: "#9ab8ff", emberDim: "rgba(122,162,247,0.25)", field: "#16161e", surface: "#1a1b26", elevated: "#1f2335", hover: "#292e42", border: "#3b4261", ink1: "#c0caf5", ink2: "#a9b1d6", ink3: "#787c99", inkMuted: "#565f89", }, light: { ember: "#2e7de9", emberGlow: "#1659c7", emberDim: "rgba(46,125,233,0.18)", field: "#e1e2e7", surface: "#e9eaf0", elevated: "#f7f8fc", hover: "#d0d5e3", border: "#a8aecb", ink1: "#343b58", ink2: "#565a6e", ink3: "#6c6e75", inkMuted: "#848cb5", }, }, { id: "solarized", label: "Solarized", dark: { ember: "#c96f47", emberGlow: "#d48a6a", emberDim: "rgba(201,111,71,0.25)", field: "#002b36", surface: "#073642", elevated: "#0a4250", hover: "#11505f", border: "#586e75", ink1: "#e2dfd0", ink2: "#b7c0bf", ink3: "#93a1a1", inkMuted: "#839496", }, light: { ember: "#2280c2", emberGlow: "#1b6aa3", emberDim: "rgba(34,128,194,0.18)", field: "#eee8d5", surface: "#fdf6e3", elevated: "#fffbef", hover: "#efe8d3", border: "#cdc4ab", ink1: "#073642", ink2: "#4f6369", ink3: "#586e75", inkMuted: "#728589", }, }, { id: "dracula", label: "Dracula", dark: { ember: "#bd93f9", emberGlow: "#d6b5ff", emberDim: "rgba(189,147,249,0.25)", field: "#21222c", surface: "#282a36", elevated: "#343746", hover: "#44475a", border: "#565a75", ink1: "#f8f8f2", ink2: "#d8d8d2", ink3: "#b0b3c5", inkMuted: "#6272a4", }, // No official Dracula light: cool slate paper + the purple darkened until it // holds as accent text, inks pulled down to near-black. light: { ember: "#7c3aed", emberGlow: "#5b21b6", emberDim: "rgba(124,58,237,0.18)", field: "#e8e8ee", surface: "#f0f0f5", elevated: "#fbfbfd", hover: "#dfdfe8", border: "#c9c9d6", ink1: "#16161e", ink2: "#34343f", ink3: "#5a5a68", inkMuted: "#787885", }, }, { id: "nord", label: "Nord", dark: { ember: "#88c0d0", emberGlow: "#a8d8e8", emberDim: "rgba(136,192,208,0.25)", field: "#272c36", surface: "#2e3440", elevated: "#3b4252", hover: "#434c5e", border: "#4c566a", ink1: "#eceff4", ink2: "#d8dee9", ink3: "#aeb6c5", inkMuted: "#7b88a1", }, // Snow Storm surfaces; accent switches to the darker frost blue (nord10) — // nord8 cyan has no contrast on paper. light: { ember: "#5e81ac", emberGlow: "#44688f", emberDim: "rgba(94,129,172,0.18)", field: "#e5e9f0", surface: "#eceff4", elevated: "#f8f9fb", hover: "#d8dee9", border: "#c2c9d6", ink1: "#2e3440", ink2: "#3b4252", ink3: "#4c566a", inkMuted: "#6b7589", }, }, { id: "rose-pine", label: "Rosé Pine", dark: { ember: "#c4a7e7", emberGlow: "#d9c4f2", emberDim: "rgba(196,167,231,0.25)", field: "#191724", surface: "#1f1d2e", elevated: "#26233a", hover: "#34304e", border: "#403d52", ink1: "#e0def4", ink2: "#c5c2dd", ink3: "#908caa", inkMuted: "#6e6a86", }, // Rosé Pine Dawn. light: { ember: "#907aa9", emberGlow: "#6f598c", emberDim: "rgba(144,122,169,0.18)", field: "#f2e9e1", surface: "#faf4ed", elevated: "#fffaf3", hover: "#ebdfd4", border: "#cecacd", ink1: "#575279", ink2: "#635e87", ink3: "#6e6a88", inkMuted: "#857f96", }, }, { id: "everforest", label: "Everforest", dark: { ember: "#a7c080", emberGlow: "#c3d6a2", emberDim: "rgba(167,192,128,0.25)", field: "#232a2e", surface: "#2d353b", elevated: "#343f44", hover: "#3d484d", border: "#475258", ink1: "#d3c6aa", ink2: "#bfb6a3", ink3: "#9da9a0", inkMuted: "#859289", }, // Toned down from the canonical light palette: the chartreuse green // (#8da101) and bg1-deep paper read harsh — muted moss accent, one step // lighter paper, darker neutral ink. light: { ember: "#6f8352", emberGlow: "#56683f", emberDim: "rgba(111,131,82,0.18)", field: "#ebeadb", surface: "#f5f4e8", elevated: "#fbfbf4", hover: "#e5e4d3", border: "#d3d2bf", ink1: "#3d484e", ink2: "#55636b", ink3: "#66705f", inkMuted: "#7a8577", }, }, { id: "kanagawa", label: "Kanagawa", dark: { ember: "#7e9cd8", emberGlow: "#9fb9ec", emberDim: "rgba(126,156,216,0.25)", field: "#16161d", surface: "#1f1f28", elevated: "#2a2a37", hover: "#363646", border: "#54546d", ink1: "#dcd7ba", ink2: "#c8c093", ink3: "#a6a69c", inkMuted: "#727169", }, // Kanagawa Lotus, the canonical light sibling, with the yellow-tan paper // desaturated to a soft cream. light: { ember: "#4d699b", emberGlow: "#38537f", emberDim: "rgba(77,105,155,0.18)", field: "#e9e5d6", surface: "#f3f0e4", elevated: "#faf8f1", hover: "#e2ddcb", border: "#d2ccb6", ink1: "#545464", ink2: "#66667a", ink3: "#716e61", inkMuted: "#8a8775", }, }, { id: "one-dark", label: "One Dark", // Upstream card (#21252b) is darker than its bg — surfaces reordered by luminance. dark: { ember: "#61afef", emberGlow: "#8cc7ff", emberDim: "rgba(97,175,239,0.25)", field: "#21252b", surface: "#282c34", elevated: "#2f343e", hover: "#3e4452", border: "#4b5263", ink1: "#d7dae0", ink2: "#abb2bf", ink3: "#8b919e", inkMuted: "#5c6370", }, // One Light (Atom's sibling theme). light: { ember: "#4078f2", emberGlow: "#2c5dd4", emberDim: "rgba(64,120,242,0.18)", field: "#eaeaeb", surface: "#fafafa", elevated: "#ffffff", hover: "#e0e0e2", border: "#d4d4d6", ink1: "#383a42", ink2: "#50525a", ink3: "#696c77", inkMuted: "#a0a1a7", }, }, { id: "night-owl", label: "Night Owl", dark: { ember: "#82aaff", emberGlow: "#a8c4ff", emberDim: "rgba(130,170,255,0.25)", field: "#011627", surface: "#0b2942", elevated: "#13344f", hover: "#1d3b53", border: "#5f7e97", ink1: "#d6deeb", ink2: "#b8c5d6", ink3: "#8ba3b8", inkMuted: "#637777", }, // Light Owl (the official light variant): teal accent, soft grays. light: { ember: "#0c969b", emberGlow: "#0a7479", emberDim: "rgba(12,150,155,0.18)", field: "#ededed", surface: "#f6f6f6", elevated: "#fbfbfb", hover: "#e2e2e2", border: "#d0d0d0", ink1: "#403f53", ink2: "#545167", ink3: "#6f6e85", inkMuted: "#989fb1", }, }, { id: "monokai", label: "Monokai Pro", dark: { ember: "#ffd866", emberGlow: "#ffe6a0", emberDim: "rgba(255,216,102,0.25)", field: "#221f22", surface: "#2d2a2e", elevated: "#403e41", hover: "#4a484b", border: "#5b595c", ink1: "#fcfcfa", ink2: "#d9d8d6", ink3: "#939293", inkMuted: "#727072", }, // Monokai Pro is dark-only: warm gray paper, yellow deepened to amber so // accent text survives on light. light: { ember: "#c08a00", emberGlow: "#856000", emberDim: "rgba(192,138,0,0.18)", field: "#e9e6e4", surface: "#f1efed", elevated: "#fcfbfa", hover: "#e0dcda", border: "#cdc8c5", ink1: "#2c292d", ink2: "#46434a", ink3: "#6b686d", inkMuted: "#8d8a8d", }, }, { id: "github", label: "GitHub", // Upstream --border (#1b1f23) is darker than the bg — swapped for a visible // Primer hairline. dark: { ember: "#58a6ff", emberGlow: "#85bdff", emberDim: "rgba(88,166,255,0.25)", field: "#1f2428", surface: "#24292e", elevated: "#2b3138", hover: "#2f363d", border: "#444d56", ink1: "#e1e4e8", ink2: "#d1d5da", ink3: "#959da5", inkMuted: "#6a737d", }, light: { ember: "#0366d6", emberGlow: "#044289", emberDim: "rgba(3,102,214,0.18)", field: "#f0f2f4", surface: "#f6f8fa", elevated: "#ffffff", hover: "#e8eaed", border: "#d1d5da", ink1: "#24292e", ink2: "#444d56", ink3: "#586069", inkMuted: "#6a737d", }, }, { id: "ayu", label: "Ayu", // Upstream --border (#1b1f29) vanishes on these surfaces — bumped two steps. dark: { ember: "#e6b450", emberGlow: "#f0cd85", emberDim: "rgba(230,180,80,0.25)", field: "#0d1017", surface: "#10141c", elevated: "#141821", hover: "#1d2330", border: "#2d3343", ink1: "#bfbdb6", ink2: "#a8a6a0", ink3: "#8a9199", inkMuted: "#6c7380", }, // ayu-light: the yellow deepens to amber for fills, and accent text (glow) // drops to a burnt orange for contrast. light: { ember: "#c97612", emberGlow: "#a65e0c", emberDim: "rgba(201,118,18,0.18)", field: "#eff1f3", surface: "#f8f9fa", elevated: "#fcfcfc", hover: "#e7eaed", border: "#d8dde2", ink1: "#3d4149", ink2: "#5c6166", ink3: "#787b80", inkMuted: "#8a9199", }, }, { id: "vitesse", label: "Vitesse", // Upstream borders (#252525 / #f0f0f0) are near-invisible — bumped a step. dark: { ember: "#4d9375", emberGlow: "#6fb392", emberDim: "rgba(77,147,117,0.25)", field: "#121212", surface: "#181818", elevated: "#1e1e1e", hover: "#262626", border: "#333333", ink1: "#dbd7ca", ink2: "#bfbaaa", ink3: "#8f8f85", inkMuted: "#758575", }, light: { ember: "#1c6b48", emberGlow: "#145236", emberDim: "rgba(28,107,72,0.18)", field: "#f0f0f0", surface: "#f7f7f7", elevated: "#ffffff", hover: "#e7e7e7", border: "#d6d6d6", ink1: "#393a34", ink2: "#4e4f47", ink3: "#6b6d63", inkMuted: "#a0ada0", }, }, { id: "synthwave", label: "Synthwave '84", // Upstream --muted-foreground is #ffffff99 — replaced with the canonical // comment lavender; elevated/hover derived (upstream card ≈ bg). dark: { ember: "#ff7edb", emberGlow: "#ffa9e7", emberDim: "rgba(255,126,219,0.25)", field: "#241b2f", surface: "#262335", elevated: "#322a47", hover: "#3d3460", border: "#495495", ink1: "#ffffff", ink2: "#d8d6e8", ink3: "#b6b1d8", inkMuted: "#848bbd", }, // Dark-only upstream: lavender-tinted paper, magenta deepened for contrast. light: { ember: "#c936a6", emberGlow: "#9c2381", emberDim: "rgba(201,54,166,0.18)", field: "#e6e2f0", surface: "#efecf6", elevated: "#fbfafd", hover: "#ddd7ec", border: "#c5bcdd", ink1: "#241b2f", ink2: "#3d3455", ink3: "#5c5380", inkMuted: "#7d76a3", }, }, ]; export const COLOR_THEME_IDS = COLOR_THEMES.map((t) => t.id); function paletteVars(p: Palette): string { return ( `--ember: ${p.ember}; --ember-glow: ${p.emberGlow}; --ember-dim: ${p.emberDim}; ` + `--field: ${p.field}; --surface: ${p.surface}; --elevated: ${p.elevated}; ` + `--hover: ${p.hover}; --border: ${p.border}; ` + `--ink-1: ${p.ink1}; --ink-2: ${p.ink2}; --ink-3: ${p.ink3}; --ink-muted: ${p.inkMuted};` ); } // Override blocks per non-default theme. Only the 12 color vars change — // --accent-text/fonts inherit from :root, and hljs CODE blocks keep their CDN // theme (toggled by mode, not color theme). function colorThemeCss(): string { let css = "\n/* color themes (settings > theme) */\n"; for (const t of COLOR_THEMES) { if (t.id === DEFAULT_COLOR_THEME) continue; css += `:root[data-color-theme="${t.id}"] { ${paletteVars(t.dark)} }\n`; css += `:root[data-color-theme="${t.id}"][data-theme="light"] { ${paletteVars(t.light)} }\n`; } return css; } const BASE_CSS = ` :root { /* dark (canonical) */ --ember: #e86f33; --ember-glow: #f0a070; --ember-dim: rgba(232,111,51,0.25); --field: #101114; --surface: #16181c; --elevated: #1e2025; --hover: #282a30; --border: #363840; --ink-1: #f0f1f3; --ink-2: #c2c4c9; --ink-3: #9a9da5; --ink-muted: #7d808a; --accent-text: var(--ember-glow); /* semantic success (task-done tint) — deliberately kept green (not the accent) since a ticked box reads as "done" universally; tokenized so light mode can darken it for contrast. Defined here in the base block so every color theme inherits it (themes override only the 12 accent/surface/ink vars). */ --ok: #2ea043; --ok-strong: #3fb950; --ok-on: #fff; /* Two more semantic states, same reasoning as --ok: a "changed" and a "removed" marker read universally in amber/red, so they stay off the accent ramp. */ --warn: #d29922; --danger: #f85149; /* Canvas behind a kit-themed md embed's iframe ELEMENT. Taken from kit.ts so it stays equal to its --color-background-primary pair: the frame's scrollbar track is transparent on purpose (kit.ts), so this element background is what paints the scroll gutter — a hardcoded white one read as a bright band in dark mode. Not light-dark(): the host stylesheet never declares color-scheme, so it would follow the OS instead of the theme the user pinned. */ --embed-paper: ${KIT_BG.dark}; --serif: Georgia, serif; --sans: system-ui, 'Segoe UI', sans-serif; --mono: ui-monospace, 'Cascadia Code', Consolas, monospace; } /* The webfonts download only when a stack names them, so the system preset (no attribute) fetches nothing. */ :root[data-fonts="bundled"] { --serif: 'Playfair Display', Georgia, serif; --sans: 'IBM Plex Sans', system-ui, 'Segoe UI', sans-serif; --serif-text: 'IBM Plex Serif', Georgia, serif; --mono: 'IBM Plex Mono', ui-monospace, 'Cascadia Code', Consolas, monospace; } :root[data-theme="light"] { --ember: #e86f33; --ember-glow: #a0501c; --ember-dim: rgba(184,90,32,0.18); --field: #e8e6e3; --surface: #efede9; --elevated: #fbfaf9; --hover: #e0ddd7; --border: #cfcbc4; --ink-1: #0a0a0a; --ink-2: #2c2c2c; --ink-3: #565656; --ink-muted: #767676; --accent-text: var(--ember-glow); --ok: #1a7f37; --ok-strong: #2ea043; --ok-on: #fff; --warn: #8a5000; --danger: #b3261e; --embed-paper: ${KIT_BG.light}; } * { box-sizing: border-box; } html, body { height: 100%; margin: 0; overflow: hidden; } /* Inherited properties, so this one root rule themes every scrolling pane. */ html { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--ink-muted) 55%, transparent) transparent; } body { background: var(--field); color: var(--ink-1); font-family: var(--mono); font-size: 14px; line-height: 1.5; -webkit-font-smoothing: antialiased; } /* height:100% (not 100vh): CSS zoom on :root scales 100vh ABOVE the real window height, pushing the app's bottom below the viewport where body's overflow:hidden clips it (the scroll container's end becomes unreachable). The 100% chain (html→body→app) tracks the zoomed viewport correctly. */ .app { display: flex; flex-direction: column; height: 100%; } /* top bar — borrowed from claude-code-hub: left brand, right icon actions */ .topbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 24px; border-bottom: 1px solid var(--border); background: var(--surface); flex: 0 0 auto; /* In the frameless WebView2 window this strip is the drag handle. */ -webkit-app-region: drag; user-select: none; } .topbar.collapsed { height: 0; min-height: 0; padding-top: 0; padding-bottom: 0; border-bottom: none; overflow: hidden; transition: height 0.25s cubic-bezier(0.4, 0, 0.2, 1); } .brand { display: flex; align-items: baseline; gap: 10px; overflow: hidden; } .wordmark { font-family: var(--serif); font-weight: 500; font-size: 18px; letter-spacing: -0.02em; } .wordmark .dot { color: var(--ember); } .padname { font-family: var(--serif); font-size: 15px; color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .padname:has(.hpad) { display: flex; align-items: baseline; gap: 6px; } .hpad { font: inherit; color: inherit; background: none; border: 1px solid transparent; border-radius: 8px; padding: 0 6px; cursor: pointer; white-space: nowrap; -webkit-app-region: no-drag; } .hpad:not(.primary) { font-family: var(--mono); font-size: 12px; border-color: var(--border); } .hpad .hrel { color: var(--ink-muted); font-size: 10px; letter-spacing: 0.04em; margin-right: 5px; } .hpad:hover { border-color: var(--ink-muted); } .hpad.cur { color: var(--ember-glow); } .view-actions { display: flex; align-items: center; gap: 4px; -webkit-app-region: no-drag; } .view-actions .icon-btn:is(*, :hover) { border-color: transparent; } .tb-div { width: 1px; height: 18px; margin: 0 6px; background: var(--border); } .more { position: relative; } .menu { position: absolute; right: 0; top: calc(100% + 6px); z-index: 60; min-width: 210px; padding: 4px; background: var(--elevated); border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.25); } .menu-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 7px 10px; border: 0; border-radius: 5px; background: none; color: var(--ink-2); font: inherit; font-size: 13px; text-align: left; text-decoration: none; white-space: nowrap; cursor: pointer; } .menu-item:hover { background: var(--hover); color: var(--ink-1); } .menu-item svg { width: 15px; height: 15px; flex: none; color: var(--ink-muted); } .menu-item kbd { margin-left: auto; } .icon-btn { width: 34px; height: 34px; display: flex; align-items: center; justify-content: center; background: transparent; border: 1px solid var(--border); border-radius: 6px; color: var(--ink-muted); cursor: pointer; transition: all 0.15s ease; text-decoration: none; padding: 0; } .icon-btn:hover { background: var(--hover); color: var(--ink-1); border-color: var(--ink-muted); } .icon-btn svg { width: 16px; height: 16px; } #commentsToggle.pill { width: auto; gap: 6px; padding: 0 12px 0 10px; border-radius: 999px; border-color: var(--ember-dim); background: var(--ember-dim); color: var(--accent-text); } #commentsToggle.pill:hover { border-color: var(--ember); } .cmt-count { font-family: var(--mono); font-size: 12px; font-weight: 600; line-height: 1; pointer-events: none; } .cmt-count[hidden] { display: none; } #saveCopy { position: relative; } .save-dot { position: absolute; top: -4px; right: -4px; width: 10px; height: 10px; border-radius: 50%; background: var(--ember); pointer-events: none; } .save-dot[hidden] { display: none; } /* Exports have no host: hide the reload button and the help rows for shortcuts that are dead there (.sc-live = live-viewer-only). */ html[data-export] #reloadBtn, html[data-export] .sc-live { display: none; } /* .sc-native = bound only inside the WebView2 host (data-native, set at boot); in a browser those keys stay the browser's own tab accelerators. */ html:not([data-native]) .sc-native { display: none; } #closeBtn:hover { background: var(--ember); color: #fff; border-color: var(--ember); } /* shortcuts modal */ .modal-scrim { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 50; } .modal { background: var(--elevated); border: 1px solid var(--border); border-radius: 10px; min-width: 340px; max-width: 440px; box-shadow: 0 12px 40px rgba(0,0,0,0.4); } /* diagram lightbox: enlarged mermaid SVG, fit to viewport */ #diagramModal { background: rgba(0,0,0,0.45); } /* % of the fixed inset:0 scrim, not 92vw/90vh: CSS zoom on :root makes vw/vh resolve against the UNZOOMED window (see .app), so the stage came out mis-sized — short of the window below zoom 1, overflowing it above. */ .diagram-stage { box-sizing: border-box; width: 92%; height: 90%; padding: 24px; background: var(--elevated); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 12px 40px rgba(0,0,0,0.5); overflow: hidden; cursor: grab; touch-action: none; user-select: none; } .diagram-stage:active { cursor: grabbing; } /* SVG carries a viewBox; width/height 100% + its default preserveAspectRatio (xMidYMid meet) scales it to fit the stage without distortion. Zoom/pan then layer a transform on top (transform-origin set to 0 0 in JS). */ .diagram-stage svg { display: block; width: 100% !important; height: 100% !important; max-width: none !important; max-height: none !important; } .diagram-stage img { display: block; width: 100%; height: 100%; object-fit: contain; } .diagram-close { position: fixed; top: 16px; right: 16px; z-index: 1; background: var(--elevated); border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.3); } .modal-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; border-bottom: 1px solid var(--border); font-family: var(--serif); font-size: 16px; color: var(--ink-1); } /* Paper (surface) rather than the near-white --elevated the other modals use — at this size the flat white sheet was harsh; keycaps swap to --elevated so they still read as raised against it. */ .modal-help { max-width: 1180px; background: var(--surface); } .modal-help kbd { background: var(--elevated); } /* No rule under the title here — the group headings already rule the columns. */ .modal-help .modal-head { border-bottom: none; } /* One shared grid for both halves (key | label | spacer | key | label): each key gutter is sized by its column's widest combo, and left/right rows share row tracks, so paired groups line up — a shorter group just leaves empty rows. Cells opt into a half via sc-l / sc-r; the markup is generated from SHORTCUT_PAIRS in render.ts. */ .shortcuts { margin: 0; padding: 4px 18px 18px; display: grid; grid-template-columns: max-content 1fr 24px max-content 1fr; column-gap: 14px; } .shortcuts dt.sc-l { grid-column: 1; } .shortcuts dd.sc-l { grid-column: 2; } .shortcuts dt.sc-r { grid-column: 4; } .shortcuts dd.sc-r { grid-column: 5; } .shortcuts .sc-group.sc-l { grid-column: 1 / 3; } .shortcuts .sc-group.sc-r { grid-column: 4 / 6; } .shortcuts dt { margin: 0; display: flex; align-items: center; align-self: center; gap: 4px; padding: 3px 0; } .shortcuts dd { margin: 0; font-size: 13px; color: var(--ink-2); align-self: center; padding: 3px 0; } /* Section headings carry the column: full-width rule + ink-1, so the eye finds Navigate / Zoom / Copy before it reads any single row. */ .shortcuts .sc-group { padding: 18px 0 6px; margin-bottom: 6px; font-size: 11px; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-1); border-bottom: 1px solid var(--border); } .shortcuts .sc-group.sc-first { padding-top: 2px; } /* keycap chips: surface fill + thicker bottom border reads as a key */ .shortcuts kbd, .picker-hint kbd, .menu-item kbd { display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 20px; padding: 0 5px; box-sizing: border-box; font-family: var(--mono); font-size: 11px; color: var(--ink-1); background: var(--surface); border: 1px solid var(--border); border-bottom-width: 2px; border-radius: 5px; } .shortcuts .sc-plus { color: var(--ink-muted); font-size: 11px; } /* jump-to-file picker (Shift+P) */ .picker-scrim { align-items: flex-start; padding-top: 12vh; } .picker { width: 760px; max-width: 92%; max-height: 70vh; padding: 10px; display: flex; flex-direction: column; gap: 8px; overflow: hidden; } .picker-input { width: 100%; box-sizing: border-box; padding: 9px 12px; background: var(--field); border: 1px solid var(--border); border-radius: 6px; color: var(--ink-1); font-family: var(--mono); font-size: 13px; } .picker-input:focus { outline: none; border-color: var(--ember); box-shadow: 0 0 0 3px var(--ember-dim); } .picker-input::placeholder { color: var(--ink-muted); } .picker-list { overflow-y: auto; display: flex; flex-direction: column; gap: 2px; } .picker-row { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 8px; background: none; border: 1px solid transparent; border-radius: 5px; color: var(--ink-2); font-family: var(--mono); font-size: 12px; text-align: left; cursor: pointer; } .picker-row .ficon { flex: none; width: 15px; height: 15px; color: var(--ink-muted); } .picker-row.current .ficon { color: var(--ember); } .picker-row:hover { background: var(--hover); } .picker-row.selected { background: var(--surface); border-color: var(--ember); } .picker-row.current .picker-name { color: var(--accent-text); } .picker-name, .picker-path, .picker-pad, .picker-group { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .picker-group { flex: none; max-width: 25%; color: var(--ink-muted); font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; } .picker-name { flex: none; max-width: 55%; color: var(--ink-1); } .picker-path { flex: 1; min-width: 0; color: var(--ink-muted); font-size: 11px; } .picker-pad { flex: none; max-width: 25%; padding: 1px 5px; border-radius: 3px; background: var(--hover); color: var(--ink-3); font-size: 11px; } .picker-empty { padding: 20px 8px; color: var(--ink-muted); font-size: 12px; text-align: center; } .picker-hint { display: flex; align-items: center; gap: 5px; color: var(--ink-muted); font-size: 11px; } /* layout */ .body { display: flex; flex: 1; min-height: 0; position: relative; } .sidebar { width: var(--tree-w, 340px); flex: 0 0 auto; min-height: 0; display: flex; flex-direction: column; background: var(--surface); border-right: 1px solid var(--border); overflow: hidden; position: relative; } /* Collapse control rides the pane's top-right corner, next to what it collapses. It overlays the tree's first line — a short small-caps group header — so a compact button with the pane's own background never collides with text. The right offset clears the tree's scrollbar, which shares that edge. */ #sidebarToggle { position: absolute; top: 8px; right: 16px; z-index: 2; width: 28px; height: 28px; background: var(--surface); } #sidebarToggle svg { width: 14px; height: 14px; } /* The in-pane toggle disappears with the pane; this floater (hidden unless collapsed) is the way back. */ #sidebarOpen { display: none; position: absolute; top: 8px; left: 8px; z-index: 6; width: 28px; height: 28px; background: var(--surface); } #sidebarOpen svg { width: 14px; height: 14px; } .sidebar.collapsed ~ #sidebarOpen { display: flex; } /* Collapse zeroes the width instantly (no transition). Rows are nowrap+ellipsis, so the clipped width never reflows mid-collapse. */ .sidebar.collapsed { width: 0; border-right: none; } .sidebar.collapsed + .resizer { display: none; } /* data-solo = a one-file save (Ctrl+Shift+S): nothing left for a tree to list. !important beats the .collapsed ~ #sidebarOpen rule above. */ :root[data-solo] .sidebar, :root[data-solo] .resizer, :root[data-solo] #sidebarOpen { display: none !important; } /* Narrow layout (data-narrow, set by the client from the zoomed width): the sidebar slides over the preview as a drawer, top bar actions move into ⋯. */ #drawerBtn { width: 30px; height: 30px; flex: none; align-self: center; border-color: transparent; -webkit-app-region: no-drag; } :root:not([data-narrow]) :is(#drawerBtn, .narrow-only), :root[data-solo] #drawerBtn { display: none; } .drawer-scrim { display: none; position: absolute; inset: 0; z-index: 39; background: rgba(0,0,0,0.4); } :root[data-narrow] .topbar { padding: 8px 10px; gap: 8px; } :root[data-narrow] .brand { align-items: center; gap: 6px; } :root[data-narrow] :is(.wordmark, #reloadBtn, #themeToggle, #settingsBtn, .tb-div, .resizer, #sidebarOpen) { display: none; } :root[data-narrow] .sidebar { position: absolute; top: 0; bottom: 0; left: 0; z-index: 40; width: min(320px, 85%); border-right: 1px solid var(--border); box-shadow: 0 12px 40px rgba(0,0,0,0.35); transform: translateX(-100%); visibility: hidden; transition: transform 0.2s ease, visibility 0s linear 0.2s; } :root[data-drawer] .sidebar { transform: none; visibility: visible; transition: transform 0.2s ease; } :root[data-drawer] .drawer-scrim { display: block; } :root[data-narrow] .topbar.collapsed ~ .body #sidebarOpen { display: flex; } :root[data-narrow] .toc { display: none !important; } :root[data-narrow] .preview { padding: 10px 8px 0; } :root[data-narrow] .pbody { padding: 18px 16px; margin-bottom: 10px; border-radius: 8px; } :root[data-narrow] .modal { min-width: 0; max-width: calc(100% - 24px); max-height: calc(100% - 24px); overflow-y: auto; } :root[data-narrow] .cmt-pop { max-width: calc(100% - 16px); } .tree { flex: 1; overflow-y: auto; padding: 14px 10px; } /* App version, pinned to the sidebar foot (bottom-left). */ .appver { flex: 0 0 auto; padding: 6px 12px; border-top: 1px solid var(--border); font-family: var(--mono); font-size: 11px; color: var(--ink-muted); } /* Drag handle between sidebar and preview. A wide hit area (easy to grab) with a thin centered visual line that brightens on hover/drag. */ .resizer { flex: 0 0 6px; cursor: col-resize; position: relative; background: transparent; margin: 0 -3px; z-index: 5; user-select: none; touch-action: none; } :is(.resizer, .mgrip)::after { content: ""; position: absolute; inset: 0 auto 0 50%; width: 1px; transform: translateX(-50%); background: var(--border); transition: background 0.15s ease; } :is(.resizer, .mgrip):is(:hover, .dragging)::after { background: var(--ember); width: 2px; } /* Recessed margin field. The optional grid (settings > background) is an engineering-notebook texture drawn from the ink ramp so it tracks every color theme + mode; it reads only in the margins around the raised .pbody card. The pattern is attribute-driven (data-grid on ) — default dots, see below. */ /* padding-bottom omitted on purpose: a scroll container's bottom padding is dropped from the scroll range (Chromium/WebView2), clipping the last line. The bottom gap lives on .pbody's margin instead, which the scroll range keeps. */ .preview { flex: 1; min-width: 0; overflow-y: auto; padding: 28px 28px 0; --grid: color-mix(in srgb, var(--ink-muted) 30%, transparent); background-color: var(--field); background-size: 22px 22px; } /* Focusable so keyboard / Vimium scrolling targets it (the only scroll container); the pane itself shouldn't show a focus ring. */ .preview:focus, .preview:focus-visible { outline: none; } /* off = no image (flat field). dots = intersections; lines = crosshatch (a touch fainter since lines cover more area). */ :root[data-grid="dots"] .preview { background-image: radial-gradient(circle, var(--grid) 1px, transparent 1.5px); } :root[data-grid="lines"] .preview { --grid: color-mix(in srgb, var(--ink-muted) 18%, transparent); background-image: linear-gradient(to right, var(--grid) 1px, transparent 1px), linear-gradient(to bottom, var(--grid) 1px, transparent 1px); } /* Single centered reading column, lifted onto a card surface one step up from the margin field so it pops in both dark & light. Everything inside shares one left edge and fills the column width (rendered markdown AND raw alike). */ /* Fit-content width: the .mspan spacer sets it to the reading measure (below), centered, up to a cap. */ .pbody { position: relative; width: fit-content; max-width: 100%; margin: 0 auto 28px; padding: 34px 44px; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 1px 2px rgba(0,0,0,0.08), 0 6px 20px rgba(0,0,0,0.12); } /* The readable measure. Wide mode (settings > width) lifts the cap (--measure: none) so the column uses the window, and widens the card itself (still leaving a margin for the grid). Registered so the ch value resolves to a length where it is declared and children inherit that length, not a count of their own characters. Declared on .md as well so the prose cap follows the reading font and size, not the header's mono. --measure-ch is the user's width (config ui.measure); the 70 fallback must match MEASURE_DEFAULT in config.ts. */ @property --measure { syntax: ' | none'; inherits: true; initial-value: none; } :is(.pbody, .md, .mspan) { --measure: calc(var(--measure-ch, 70) * 1ch); } :root[data-wide] :is(.pbody, .md, .mspan) { --measure: none; } :is(:root[data-wide] .pbody, .pbody.fill) { width: auto; max-width: 95%; } .mgrip { position: absolute; top: 12px; bottom: 12px; right: -4px; width: 8px; cursor: ew-resize; z-index: 2; } .mgrip::after { background: transparent; } :root[data-wide] .mgrip { display: none; } .pbody > .pintro { max-width: var(--measure); } /* Zero-height spacer that holds the card at least the measure wide, in the reading font for every file kind, so the set width is the same on short pages, code and text. A % max-width is ignored when the fit-content card sizes itself, then clamps the spacer to the card. */ .mspan { height: 0; width: var(--measure); max-width: 100%; } /* The body adds nothing to the card's width, so the card is the measure on every page: wide code and tables scroll inside it and images scale down, and the grip on the card edge is also the prose edge. */ .pbody > :is(.md, pre.code, .cb) { contain: inline-size; } /* Table of contents: an opaque on-demand panel floating in the preview's right gutter, pinned over the .body area so it sits below the topbar and stays put while the preview scrolls underneath. Off by default; JS (updateToc) toggles display when the user invokes it ('o' / settings) and the file has ≥2 headings. Opaque (own surface + shadow) so it reads even where it overlaps the card; the right offset clears the preview's scrollbar. Full H1–H6 hierarchy, full names (links wrap rather than truncate). */ /* Width tracks the longest heading (fit-content) up to a cap. Entries never * wrap; anything past the cap is ellipsis-truncated (full name on hover via * title) rather than scrolling — so the whole list is always visible at once. */ .toc { position: absolute; top: 16px; right: 24px; width: fit-content; min-width: 200px; max-width: min(48vw, 460px); max-height: calc(100% - 32px); overflow-x: hidden; overflow-y: auto; z-index: 4; display: none; padding: 14px 16px; background: var(--elevated); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 1px 2px rgba(0,0,0,0.08), 0 6px 20px rgba(0,0,0,0.18); } /* Docked: a plain rail in the gutter the preview padding reserves. --toc-rail and the rail width are set by updateToc in render.ts. */ :root[data-toc-dock] .preview { padding-right: var(--toc-rail); } :root[data-toc-dock] .toc { min-width: 0; padding: 4px 0; background: var(--field); border: 0; border-radius: 0; box-shadow: none; } .toc-empty { padding: 3px 10px; font-size: 12.5px; color: var(--ink-muted); } .toc-head { font-size: 10px; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-muted); padding: 0 0 8px 2px; } .toc-nav { display: flex; flex-direction: column; border-left: 1px solid var(--border); } .toc-link { display: block; padding: 3px 8px; border-left: 2px solid transparent; margin-left: -1px; font-size: 12.5px; line-height: 1.35; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--ink-3); text-decoration: none; } /* Indented by heading depth; deeper levels read progressively quieter. */ .toc-link.toc-h1 { padding-left: 8px; } .toc-link.toc-h2 { padding-left: 20px; } .toc-link.toc-h3 { padding-left: 32px; font-size: 12px; color: var(--ink-muted); } .toc-link.toc-h4 { padding-left: 44px; font-size: 12px; color: var(--ink-muted); } .toc-link.toc-h5 { padding-left: 56px; font-size: 11.5px; color: var(--ink-muted); } .toc-link.toc-h6 { padding-left: 68px; font-size: 11.5px; color: var(--ink-muted); } .toc-link:hover { color: var(--ink-1); } .toc-link.active { color: var(--ember-glow); border-left-color: var(--ember); } /* Jumping to a heading (TOC click / #anchor) leaves a line of breathing room * above it instead of pinning it flush to the top edge. */ .md :is(h1, h2, h3, h4, h5, h6) { scroll-margin-top: 1.6em; } /* tree */ /* Scoped to .tree deliberately: mermaid tags every node/edge label in its SVG with class="label", so an unscoped rule upper-cased and letter-spaced every diagram label — and since mermaid sizes each box by measuring the raw string, the wider painted text then overflowed the box. */ .tree .label { font-size: 11px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-muted); padding: 6px 10px; } /* Stacked group headers: separate each group from the rows above it. The first group sits flush at the top; only subsequent ones get the gap. */ .ggroup + .ggroup .glabel { margin-top: 14px; } /* Collapsible group header: caret + label, click/Enter toggles its rows. */ .glabel { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; } .glabel:hover { color: var(--ink-2); } .gcaret { flex: none; width: 0; height: 0; border-left: 4px solid currentColor; border-top: 3.5px solid transparent; border-bottom: 3.5px solid transparent; opacity: 0.7; transform: rotate(90deg); transition: transform 0.12s ease; } .ggroup.collapsed .gcaret { transform: rotate(0deg); } .ggroup.collapsed .grows { display: none; } /* Pad set: one collapsible section per pad, holding that pad's groups. */ .padsec + .padsec { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border); } .tree .plabel { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; font-size: 12.5px; letter-spacing: 0.02em; text-transform: none; font-weight: 600; color: var(--ink-2); } .plabel .pname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .padsec.cur > .plabel { color: var(--ember-glow); } .prel { margin-left: auto; flex: none; font-size: 10px; font-weight: 500; letter-spacing: 0.04em; color: var(--ink-muted); border: 1px solid var(--border); border-radius: 8px; padding: 0 6px; } .padsec.collapsed > .plabel .gcaret { transform: rotate(0deg); } .padsec.collapsed > .prows { display: none; } .prows .ggroup .glabel { padding-left: 18px; } .frow { display: flex; align-items: baseline; gap: 9px; cursor: pointer; padding: 6px 12px; border-radius: 5px; font-size: 14px; color: var(--ink-2); transition: all 0.15s ease; } .frow:hover { background: var(--hover); } /* Active row: neutral fill + thin ember bar (a tinted pill read as noise). The bar sits flush against the squared-off left edge; the radius stays on the right. */ .frow.active { color: var(--ink-1); background: var(--hover); box-shadow: inset 2px 0 0 var(--ember); border-radius: 0 5px 5px 0; } .frow.unreg { color: var(--ink-muted); font-style: italic; } /* Session-revealed hidden file: semi-transparent so it reads as "here on loan" — the manifest still marks it hidden and a relaunch hides it again. */ .frow.revealed { opacity: 0.55; } .frow.revealed.active { opacity: 0.8; } /* file-kind glyph: currentColor so it dims with the row and lights up on the active row (ember), sitting a step muted at rest. align-self:center since the row baseline-aligns its text. */ .frow .ficon { flex: none; width: 15px; height: 15px; align-self: center; color: var(--ink-muted); opacity: 0.85; } .frow:hover .ficon, .frow.active .ficon { opacity: 1; } .frow.active .ficon { color: var(--ember); } .frow .fttl { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .frow .fdot { display: none; flex: none; width: 6px; height: 6px; border-radius: 50%; align-self: center; background: var(--ember); } :root:not([data-comments-off]) .frow.hascmt .fdot { display: block; } /* preview */ .phead { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 12px; margin-bottom: 12px; } .pfile { font-family: var(--mono); font-size: 12px; color: var(--ink-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pfile b { font-weight: 500; color: var(--accent-text); } .pdates { margin-left: auto; font-family: var(--mono); font-size: 11px; color: var(--ink-muted); white-space: nowrap; flex-shrink: 0; } .ptitle { font-family: var(--serif); font-weight: 500; font-size: 26px; line-height: 1.2; margin: 0 0 8px; } .pmeta { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; } .pmeta .chip { font-family: var(--mono); font-size: 11px; line-height: 1; padding: 3px 7px; border-radius: 999px; border: 1px solid var(--border); color: var(--ink-muted); } .pmeta .chip.ctype { color: var(--accent-text); border-color: var(--ember-dim); } .pdesc { font-family: var(--read); color: var(--ink-3); font-size: 14.5px; margin: 0; } .pintro { margin-bottom: 28px; } .pintro > :last-child { margin-bottom: 0; } /* markdown — fills the reading column (width governed by .pbody). Body copy sits one step down the ink ramp so bold (full-strength + heavier) clearly stands out; heading levels are color-coded down the ember ramp for at-a-glance hierarchy. */ /* Single sizing knob: heading sizes below are em-relative, so adjusting this one font-size scales the whole reading column proportionally. */ :root { --read: var(--mono); --read-size: 16px; --read-lh: 1.7; --h-font: var(--serif); --read-scale: 1; --code-size: calc(14px * var(--read-scale)); } /* The reading font switch belongs to the bundled preset only. */ :root[data-fonts="bundled"] { --read: var(--sans); --read-size: 17px; --read-lh: 1.65; --h-font: var(--read); } :root[data-fonts="bundled"][data-read="serif"] { --read: var(--serif-text); } :root[data-fonts="bundled"][data-read="mono"] { --read: var(--mono); --read-size: 16px; --read-lh: 1.7; } :root:not([data-fonts="bundled"]) #readSection { display: none; } :root[data-read-size="s"] { --read-scale: 0.88; } :root[data-read-size="l"] { --read-scale: 1.12; } :root[data-read-size="xl"] { --read-scale: 1.25; } :is(.md, .mspan) { font-family: var(--read); font-size: calc(var(--read-size) * var(--read-scale)); } .md { max-width: 100%; color: var(--ink-2); line-height: var(--read-lh); overflow-wrap: break-word; } .md strong, .md b { font-weight: 700; color: var(--ink-1); } .md del { color: var(--ink-3); } /* Display serif only where it is large; its hairlines fade at body sizes. */ .md h1, .md h2, .md h3, .md h4, .md h5, .md h6 { font-family: var(--h-font); font-weight: 600; line-height: 1.25; margin: 1.4em 0 0.5em; } .md h1, .md h2 { font-family: var(--serif); } /* Stepped progression: h1 accent, then a uniform size + ink-ramp descent. */ .md h1 { font-size: 1.625em; color: var(--ember-glow); } .md h2 { font-size: 1.3125em; color: var(--ink-1); border-bottom: 1px solid var(--border); padding-bottom: 0.25em; } .md h3 { font-size: 1.125em; color: var(--ink-1); } .md h4 { font-size: 1em; color: var(--ink-2); } .md h5 { font-size: 0.875em; color: var(--ink-3); } .md h6 { font-size: 0.8125em; color: var(--ink-muted); } .md p { margin: 0.7em 0; } .md a { color: var(--accent-text); text-decoration: none; border-bottom: 1px solid var(--ember-dim); } .md ul, .md ol { padding-left: 1.4em; margin: 0.6em 0; } .md li { margin: 0.2em 0; } /* GFM task list items: hanging checkbox, checked ones tinted green. NOT a flex container — flex would make every inline fragment (each text run AND each inline ) its own flex item, scattering the sentence. The text must flow as normal inline content; only the checkbox hangs (absolute). */ .md li.task { list-style: none; margin-left: -1.4em; padding-left: 1.6em; position: relative; } /* A heading can be a task too ("## [ ] todo") — same hanging checkbox, sized in em so it tracks the heading's own font-size. */ .md :is(h1,h2,h3,h4,h5,h6).task { padding-left: 1.4em; position: relative; } .md .task > .chk { position: absolute; left: 0; top: 0.28em; display: inline-flex; align-items: center; justify-content: center; width: 1.05em; height: 1.05em; border: 1.5px solid var(--ink-muted); border-radius: 3px; font-size: 0.8em; color: transparent; cursor: pointer; transition: border-color 0.12s, background 0.12s; } .md .task > .chk:hover { border-color: var(--ok); } .md .task.done > .chk { background: var(--ok); border-color: var(--ok); color: var(--ok-on); } .md .task.done > .chk:hover { background: var(--ok-strong); border-color: var(--ok-strong); } .md .task.done { color: var(--ink-3); } .md blockquote { border-left: 2px solid var(--border); margin: 0.8em 0; padding: 0 0 0 14px; color: var(--ink-3); } /* GFM alerts (> [!NOTE] etc): the five GitHub types. Each sets --alert, which tints the border and the icon+title row; the body keeps normal reading ink. tip/warning/caution reuse the semantic tokens so every color theme inherits them; note/important have no palette counterpart, so like code.cs's --cs-type they carry their own blue/purple with a light-mode override for contrast. */ .md blockquote.alert { --alert: var(--ink-3); position: relative; padding: 8px 12px 8px 36px; border: 1px solid color-mix(in srgb, var(--alert) 30%, transparent); border-radius: 6px; background: color-mix(in srgb, var(--alert) 9%, transparent); color: var(--ink-2); } .md blockquote.alert > * { margin: 0.5em 0; } .md blockquote.alert > :is(:first-child, .alert-title + *) { margin-top: 0; } .md blockquote.alert > :last-child { margin-bottom: 0; } .md blockquote.alert > .alert-title { float: left; margin: 0 0.5em 0 0; font-weight: 600; color: var(--alert); } .md blockquote.alert > .alert-title + :not(p) { clear: left; margin-top: 0.4em; } .md .alert-title .alert-icon { position: absolute; left: 12px; top: calc(8px + (1lh - 16px) / 2); } .md blockquote.alert-note { --alert: #4493f8; } .md blockquote.alert-important { --alert: #ab7df8; } :root[data-theme="light"] .md blockquote.alert-note { --alert: #0969da; } :root[data-theme="light"] .md blockquote.alert-important { --alert: #8250df; } .md blockquote.alert-tip { --alert: var(--ok-strong); } .md blockquote.alert-warning { --alert: var(--warn); } .md blockquote.alert-caution { --alert: var(--danger); } /* HTML comments kept visible but set apart — muted and italic, like a source comment. pre-wrap keeps a multi-line comment in the shape it was written in; on the mid-line form it has nothing to preserve. Unrelated to the viewer's own inline-comment feature, which owns the .cmt-* classes. */ .md .md-comment { color: var(--ink-muted); font-style: italic; white-space: pre-wrap; } .md hr { border: 0; border-top: 1px solid var(--border); margin: 1.4em 0; } .md code { font-family: var(--mono); font-size: 0.82em; background: color-mix(in srgb, var(--ink-muted) 12%, transparent); border: 1px solid color-mix(in srgb, var(--border) 60%, transparent); border-radius: 3px; padding: 0.05em 0.3em; } /* The code metrics live on
, not on 
, because the line-number
   gutter is a SIBLING of  — putting them a level down leaves the gutter on
   the .md body size and it drifts a fraction of a line per row. */
.md pre { background: color-mix(in srgb, var(--ink-muted) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  border-radius: 6px; padding: 12px 14px; overflow-x: auto; margin: 0.9em 0;
  font-family: var(--mono); font-size: var(--code-size); line-height: 1.55; }
/* inherit, not a value: .md code's 0.82em would otherwise shrink fenced code. */
.md pre code { background: none; border: 0; padding: 0; font-size: inherit; }
/* Fences tagged \`callstack\` (paintStack): the guides recede so the identifiers
   carry the line, and the trailing change markers come forward as badges. Scoped
   to code.cs so no other fenced block is affected.
   --cs-type is the qualifier ink. It lives here rather than in the palette because
   only this block uses it, and it is cool on purpose: it has to sit opposite both
   the accent and --ok/--warn/--danger, or a type starts reading as a change. */
code.cs { --cs-type: #7fb3e3; }
:root[data-theme="light"] code.cs { --cs-type: #2f6099; }
code.cs .cs-g { color: color-mix(in srgb, var(--ink-muted) 60%, transparent); }
code.cs .cs-note { color: var(--ink-muted); font-style: italic; }
/* Identifier shape. The call name takes the accent because it is the one thing
   the reader tracks down the tree; the qualifier takes the cool type ink; the
   arguments recede furthest. Declared before .cs-lit so a literal sitting inside
   an argument list still takes its own accent. */
code.cs .cs-arg { color: color-mix(in srgb, var(--ink-muted) 85%, transparent); }
code.cs .cs-q { color: var(--cs-type); }
code.cs .cs-m { color: var(--accent-text); font-weight: 500; }
code.cs .cs-lit { color: var(--ink-3); }
/* One badge formula parameterized by --cs, so the 16%/35% tint ratio is a single
   edit rather than three. --cs-fg is the opt-out, for the two states whose text
   cannot ride the tint: cs-new needs a brighter green, cs-neu has no fill at all. */
code.cs .cs-b { --cs: var(--warn); border-radius: 3px; padding: 0 0.45em;
  font-size: 0.85em; font-weight: 600;
  color: var(--cs-fg, var(--cs));
  background: color-mix(in srgb, var(--cs) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--cs) 35%, transparent); }
code.cs .cs-new { --cs: var(--ok); --cs-fg: var(--ok-strong); }
code.cs .cs-mod { --cs: var(--warn); }
code.cs .cs-del { --cs: var(--danger); }
/* The sigil, brought back visibly. The authored one is hidden inside .cs-x for
   copy fidelity, which left colour carrying the meaning alone — a badge is now
   readable without it. Generated content is not part of textContent, so the
   copy button and the line gutter still see the exact source bytes. cs-neu
   declares no content, so an unsigilled marker still shows no glyph. */
code.cs .cs-b::before { font-weight: 700; opacity: 0.75; margin-right: 0.32em; }
code.cs .cs-new::before { content: '+'; }
code.cs .cs-mod::before { content: '~'; }
code.cs .cs-del::before { content: '-'; }
/* No sigil = no claim about change. Ink rather than a colour, and no fill, so a
   page of plain annotations does not read as a page of warnings. */
code.cs .cs-neu { --cs: var(--ink-muted); --cs-fg: var(--ink-2);
  background: transparent; }
/* The source arrow, kept in the DOM for copy fidelity but taken out of the
   painted line. font-size:0 (not display:none) so it still copies. */
code.cs .cs-x { font-size: 0; }
/* width:max-content (not 100%) so a narrow table keeps its own width; a wider one
   wraps its long cells at the card width and then scrolls. */
.md table { border-collapse: collapse; margin: 1em 0; font-size: calc(15px * var(--read-scale)); width: max-content; max-width: 100%; display: block; overflow-x: auto; }
.md th, .md td { border: 1px solid var(--border); padding: 6px 11px; text-align: left; vertical-align: top; }
.md :is(th, td).nw { white-space: nowrap; }
.md :is(th, td):not(.nw) { min-width: 20ch; }
.md thead th { background: color-mix(in srgb, var(--ink-muted) 12%, transparent); color: var(--ink-1); font-weight: 600; }
.md tbody tr:nth-child(even) { background: color-mix(in srgb, var(--ink-muted) 5%, transparent); }

/* code / raw — larger, more readable monospace for source/raw views */
pre.code { font-family: var(--mono); font-size: calc(13px * var(--read-scale)); line-height: 1.55;
  background: color-mix(in srgb, var(--ink-muted) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  border-radius: 6px; padding: 14px 16px; margin: 0;
  /* wrap long lines instead of truncating; break only at whitespace so words/
     tokens stay intact, with overflow-wrap as the last resort for unbreakable runs */
  white-space: pre-wrap; word-break: normal; overflow-wrap: anywhere; tab-size: 2; }

/* code-block chrome (decorateCodeBlocks): a header strip (language badge +
   hover-reveal Copy) wraps every 
; the figure carries the border +
   recessed surface and the inner 
 goes flat so head + body read as one panel. */
.cb { margin: 0.9em 0; border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  border-radius: 6px; overflow: hidden; background: color-mix(in srgb, var(--ink-muted) 8%, transparent); }
.cb > pre { margin: 0; border: 0; border-radius: 0; background: transparent; }
.cb-head { display: flex; align-items: center; gap: 8px;
  padding: 4px 8px 4px 14px;
  background: color-mix(in srgb, var(--ink-muted) 7%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent); }
.cb-lang { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.04em;
  text-transform: lowercase; color: var(--ink-muted); }
.cb-name { font-family: var(--mono); font-size: 11.5px; color: var(--ink-2);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cb-copy { flex: none; display: inline-flex; margin-left: auto; line-height: 1;
  color: var(--ink-muted); background: transparent; border: 1px solid transparent;
  border-radius: 4px; padding: 3px; cursor: pointer; opacity: 0;
  transition: opacity 0.12s ease, color 0.12s ease, background 0.12s ease, border-color 0.12s ease; }
.cb:hover .cb-copy, .cb-copy:focus-visible { opacity: 1; }
.cb-copy:hover { color: var(--ink-1); background: var(--hover); border-color: var(--border); }
.cb-copy.copied { opacity: 1; color: var(--ok); background: transparent; border-color: transparent; }
.cb-copy svg { width: 13px; height: 13px; }
/* line-number gutter: lives inside 
 so it inherits that context's exact
   font metrics and aligns for free;  becomes the scroll box so the gutter
   stays fixed while long lines scroll under it. */
pre.has-nos { display: flex; overflow: hidden; }
pre.has-nos > .cb-nos { flex: none; user-select: none; text-align: right;
  padding-right: 16px; color: var(--ink-muted); opacity: 0.4; }
pre.has-nos > code { flex: 1 1 auto; min-width: 0; display: block; overflow-x: auto; white-space: pre; }

.imgwrap { display: flex; justify-content: center; padding: 12px 0; }
/* server-rendered excalidraw SVGs: fill the card width (vector, scales clean) */
.imgwrap img.excalidraw { width: 100%; }

/* In-place Excalidraw editor overlay (live viewer only; see openExcalidrawEditor) */
#excaOverlay { position: fixed; inset: 0; z-index: 400; display: flex; flex-direction: column; background: var(--field); }
#excaOverlay .exca-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 12px; border-bottom: 1px solid var(--border); }
#excaOverlay .exca-file { font-family: var(--mono); font-size: 12px; color: var(--ink-3); }
#excaOverlay .exca-actions { display: flex; gap: 6px; }
#excaOverlay .exca-host { flex: 1; min-height: 0; }
#excaOverlay .exca-host .notice { padding: 24px; }
/* empty-scene placeholder: the edit entry point sits centered in the card */
.exca-empty { display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 56px 0; border: 1px dashed var(--border); border-radius: 6px; color: var(--ink-3); }
.imgwrap img { max-width: 100%; max-height: 80vh; object-fit: contain;
  border: 1px solid var(--border); border-radius: 6px; background: var(--elevated); }
.md img.mdimg { max-width: 100%; height: auto; border: 1px solid var(--border);
  border-radius: 6px; background: var(--elevated); }
.notice { color: var(--ink-muted); font-size: 13px; padding: 20px 0; }
/* #fff, not a theme token: this bare form is the standalone .html preview, an author
   page that never opted into dark and gets no kit CSS — white paper is the honest
   backdrop for it. Md embeds are kit-themed and override this (see --embed-paper). */
.htmlframe { width: 100%; height: 75vh; border: 1px solid var(--border);
  border-radius: 6px; background: #fff; }

/* Full-window mode for an html embed ('f'). The frame is NEVER reparented — a
   moved/cloned iframe reloads, losing the author page's scroll and in-page state
   — so this is pure CSS: the chrome hides and the one [data-focused] frame goes
   position:fixed over everything. Its .pbody ancestor stays displayed (display:none
   on an ancestor would kill the fixed child too); the frame simply covers it. */
:root[data-focus] .topbar, :root[data-focus] .sidebar, :root[data-focus] .resizer,
:root[data-focus] .toc, :root[data-focus] #sidebarOpen { display: none !important; }
:root[data-focus] .preview { overflow: hidden; padding: 0; }
/* 100% (of the fixed box's containing block = the viewport), NOT 100vw/100vh and NOT
   auto: vw/vh resolve against the UNZOOMED window under CSS zoom on :root (see .app),
   and an iframe is a REPLACED element, so auto means its intrinsic 300x150 — inset:0
   does not stretch it. !important to beat .md .htmlframe's width and the inline height
   the ResizeObserver sets. enterFocus also drops the zoom while focused. */
:root[data-focus] .htmlframe[data-focused] {
  position: fixed; inset: 0; z-index: 60; margin: 0;
  width: 100% !important; height: 100% !important; max-width: none;
  border: 0; border-radius: 0; }
/* Exit affordance for mouse users (the topbar is gone). Quiet until hovered so it
   doesn't sit on top of the author page's own top-right corner. */
.focus-close { position: fixed; top: 12px; right: 12px; z-index: 61; display: none;
  background: var(--elevated); border: 1px solid var(--border);
  box-shadow: 0 2px 8px rgba(0,0,0,0.3); opacity: 0.35; transition: opacity 0.15s ease; }
.focus-close:hover { opacity: 1; }
:root[data-focus] .focus-close { display: inline-flex; }

/* preview header controls */
.pctrls { display: inline-flex; align-items: center; gap: 2px; margin-left: 8px; }
.pctrls .seg { margin-right: 4px; }
.seg.sm button { font-size: 11px; padding: 3px 8px; }
.icon-btn.sm { width: 24px; height: 24px; border: 0; border-radius: 5px; }
.icon-btn.sm svg { width: 14px; height: 14px; }
.icon-btn.sm.on { color: var(--accent-text); }
.pbtn { display: inline-flex; align-items: center; gap: 4px; line-height: 1;
  background: var(--field); color: var(--ink-muted); border: 1px solid var(--border);
  border-radius: 4px; padding: 3px 8px; font-family: var(--mono); font-size: 11px; cursor: pointer;
  transition: all 0.15s ease; }
.pbtn:hover { background: var(--hover); color: var(--ink-1); }
.pbtn.on { color: var(--accent-text); border-color: var(--ember-dim); }

/* mermaid */
/* relative: the expand chip (.embed-full) parks in the diagram's corner. */
.mermaid { margin: 1em 0; text-align: center; position: relative; }
.mermaid svg { max-width: 100%; height: auto; }

/* KaTeX math. Display blocks scroll horizontally rather than overflow the card;
   the raw-source fallback (offline, no katex) inherits prose color/wrapping. */
.math-display { margin: 1em 0; overflow-x: auto; overflow-y: hidden; }
.katex-display { margin: 0; }

/* footnotes / citations (Pandoc/GFM [^id] refs + [^id]: defs) */
/* Citation ref: bracketed [n] in accent color so it reads as a clickable
   citation, not a stray digit.  already shrinks to ~0.83x, so keep our
   factor mild (the old 0.72em compounded to ~0.6em — near-invisible). The
   .md a border-bottom underline is dropped; hover restores an underline. */
sup.fnref { font-size: 0.9em; line-height: 0; margin-left: 1px; font-weight: 600; }
sup.fnref a { color: var(--accent-text); text-decoration: none; border-bottom: 0; }
sup.fnref a::before { content: "["; }
sup.fnref a::after { content: "]"; }
sup.fnref a:hover { text-decoration: underline; }
.fn-sep { margin-top: 2em; }
.footnotes { font-size: 0.9em; color: var(--ink-2); }
.footnotes ol { padding-left: 1.4em; }
.footnotes li { margin: 0.35em 0; }
.fn-back { text-decoration: none; margin-left: 0.35em; }

/* Transient highlight on the jumped-to citation target (def 
  • or ref ). Fades from an accent tint to nothing over 10s; the box-shadow ring extends the tint slightly past the element so a tight still reads as highlighted. animation-fill-mode forwards holds the cleared end state until JS strips it. */ @keyframes anchor-flash { from { background: var(--ember-dim); box-shadow: 0 0 0 4px var(--ember-dim); } to { background: transparent; box-shadow: 0 0 0 4px transparent; } } .anchor-flash { animation: anchor-flash 10s ease-out forwards; border-radius: 4px; } /* Wikilink ([[name]]) that didn't resolve to a pad file — visible but inert (not a real ), dotted-underline in muted ink so it reads as "referenced but absent" rather than a plain typo, echoing the dashed .cmt-orphans language used for the same idea elsewhere in the viewer. */ .wikilink-broken { color: var(--ink-muted); border-bottom: 1px dotted var(--ink-muted); } /* live html embeds (![](file.html)) inside rendered markdown — sandboxed iframe, height set from content. Scoped to .md so it doesn't clobber the full-file html preview (bare .htmlframe, above). max-width keeps diagrams compact. The wrapper owns the box (every md embed is wrapped) and gives the expand chip its positioning context; a span, not a div, because the embed is emitted from inline markdown, inside a p. */ .md .htmlembed { display: block; position: relative; width: 100%; max-width: 760px; margin: 1em auto; } .md .htmlembed > .htmlframe { display: block; width: 100%; height: 150px; border: 0; margin: 0; background: var(--embed-paper); } /* Set by boxEmbed (see viewportCoupled in render.ts): a slide-shaped box the page scrolls inside. Not while focused — full window owns the frame's size. */ .md .htmlembed.embed-boxed > .htmlframe:not([data-focused]) { height: auto; aspect-ratio: 16 / 10; max-height: 75vh; } /* Hover-only: an always-on chip would cover the author page's own corner. */ .embed-full { position: absolute; top: 6px; right: 6px; z-index: 1; background: var(--elevated); border: 1px solid var(--border); border-radius: 4px; color: var(--ink-muted); font-family: var(--mono); font-size: 11px; line-height: 1; padding: 4px 7px; cursor: pointer; opacity: 0; transition: opacity 0.15s ease; } /* The same chip on a mermaid diagram — one affordance for both embed kinds. */ .md .htmlembed:hover > .embed-full, .md .mermaid:hover > .embed-full, .embed-full:focus-visible { opacity: 1; } .embed-full:hover { color: var(--ink-1); } /* highlight.js — CODE blocks get a full CDN theme (github-dark / github, loaded in ). We only strip the theme's own background + padding so blocks sit on our recessed code surface; token colors come from the CDN theme. */ .hljs, pre.code > code.hljs { background: transparent; padding: 0; } /* Raw MARKDOWN source view keeps the warm Lab-Notebook palette (ink ramp + one ember), scoped to .mdsrc so it never touches the CDN-themed code blocks. */ .mdsrc.hljs { color: var(--ink-1); } .mdsrc .hljs-comment, .mdsrc .hljs-quote { color: var(--ink-muted); font-style: italic; } .mdsrc .hljs-keyword, .mdsrc .hljs-selector-tag, .mdsrc .hljs-built_in, .mdsrc .hljs-name, .mdsrc .hljs-tag { color: var(--ember-glow); } .mdsrc .hljs-string, .mdsrc .hljs-attr, .mdsrc .hljs-template-tag, .mdsrc .hljs-addition { color: var(--ink-2); } .mdsrc .hljs-number, .mdsrc .hljs-literal, .mdsrc .hljs-type, .mdsrc .hljs-symbol, .mdsrc .hljs-meta { color: var(--ink-3); } .mdsrc .hljs-title, .mdsrc .hljs-function .hljs-title { color: var(--ink-1); font-weight: 500; } .mdsrc .hljs-variable, .mdsrc .hljs-params, .mdsrc .hljs-property { color: var(--ink-2); } /* Markdown structural tokens — colorful so the raw view reads as highlighted: headers + list markers in ember, links in accent text. */ .mdsrc .hljs-section { color: var(--ember-glow); font-weight: 700; } .mdsrc .hljs-bullet { color: var(--ember); font-weight: 700; } .mdsrc .hljs-link { color: var(--accent-text); } .mdsrc .hljs-code { color: var(--ink-2); } .mdsrc .hljs-emphasis { font-style: italic; color: var(--ink-1); } .mdsrc .hljs-strong { font-weight: 700; color: var(--ink-1); } /* inline comments — highlighted spans, popover, add affordance, orphan pill */ .cmt-hl { background: color-mix(in srgb, var(--ember) 18%, transparent); border-bottom: 1px dotted var(--ember); cursor: pointer; } .cmt-hl:hover { background: color-mix(in srgb, var(--ember) 32%, transparent); } :root[data-comments-off] .cmt-hl { background: transparent; border-bottom: 0; cursor: inherit; } :root[data-comments-off] .cmt-orphans { display: none; } /* Always-visible concise note after the highlight. Rendered from data-note via ::after so the text is unselectable and invisible to quote-matching. */ .cmt-note { cursor: pointer; } .cmt-note::after { content: '💬 ' attr(data-note); margin-left: 4px; padding: 1px 7px; border-radius: 9px; background: color-mix(in srgb, var(--ember) 12%, transparent); border: 1px solid var(--ember-dim); color: var(--ink-2); font-family: var(--mono); font-size: 0.72em; font-style: normal; font-weight: 400; vertical-align: baseline; white-space: nowrap; } .cmt-note:hover::after { color: var(--accent-text); border-color: var(--ember); } :root[data-comments-off] .cmt-note { display: none; } .cmt-add { position: fixed; z-index: 9000; background: var(--elevated); color: var(--ink-1); border: 1px solid var(--ember-dim); border-radius: 5px; padding: 4px 10px; font-family: var(--mono); font-size: 11px; cursor: pointer; box-shadow: 0 4px 14px rgba(0,0,0,0.25); } .cmt-add:hover { color: var(--accent-text); border-color: var(--ember); } .cmt-pop { position: fixed; z-index: 9001; width: 300px; background: var(--elevated); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; box-shadow: 0 6px 22px rgba(0,0,0,0.3); font-size: 13px; color: var(--ink-1); } .cmt-pop .cmt-body { white-space: pre-wrap; word-break: break-word; } .cmt-pop .cmt-when { margin-top: 6px; font-family: var(--mono); font-size: 10px; color: var(--ink-muted); } .cmt-pop .cmt-quote { font-family: var(--mono); font-size: 11px; color: var(--ink-muted); font-style: italic; margin-bottom: 4px; word-break: break-word; } .cmt-pop textarea { width: 100%; min-height: 64px; box-sizing: border-box; resize: vertical; background: var(--field); color: var(--ink-1); border: 1px solid var(--border); border-radius: 4px; padding: 6px 8px; font-family: inherit; font-size: 13px; } .cmt-actions { display: flex; gap: 6px; justify-content: flex-end; margin-top: 8px; } .cmt-orow { padding: 6px 0; } .cmt-orow + .cmt-orow { border-top: 1px solid var(--border); } .cmt-pop.cmt-summary { width: 340px; max-height: 60vh; overflow-y: auto; padding: 12px; } .cmt-shead { font-family: var(--mono); font-size: 10px; color: var(--ink-muted); text-transform: uppercase; letter-spacing: 0.06em; padding-bottom: 8px; border-bottom: 1px solid var(--border); margin-bottom: 4px; } .cmt-sfile { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 600; color: var(--ink-muted); text-transform: uppercase; letter-spacing: 0.04em; margin: 12px 0 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .cmt-sfile::before { content: '📄'; font-size: 11px; filter: grayscale(0.3); } .cmt-spad { flex: none; text-transform: none; letter-spacing: 0; font-weight: 500; color: var(--ink-3); border: 1px solid var(--border); border-radius: 4px; padding: 0 4px; } .cmt-srow { cursor: pointer; border-radius: 6px; padding: 8px; margin: 0 -4px; transition: background 0.12s ease; } .cmt-srow:hover { background: var(--hover); } .cmt-srow .cmt-body { font-size: 13px; color: var(--ink-1); line-height: 1.4; } .cmt-srow .cmt-quote { margin: 5px 0 0; padding: 1px 0 1px 8px; border-left: 2px solid var(--ember-dim); font-style: normal; color: var(--ink-muted); } .cmt-srow .cmt-when { margin-top: 5px; } .cmt-orphans { display: inline-flex; align-items: center; gap: 6px; margin: 6px 0 10px; font-family: var(--mono); font-size: 11px; color: var(--ink-muted); border: 1px dashed var(--border); border-radius: 5px; padding: 4px 10px; cursor: pointer; } .cmt-orphans:hover { color: var(--ink-1); border-color: var(--ember-dim); } .icon-btn.muted { opacity: 0.45; } /* empty states */ .empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--ink-muted); text-align: center; gap: 8px; } .empty .big { font-family: var(--serif); font-size: 20px; color: var(--ink-3); } .empty code { background: var(--field); border: 1px solid var(--border); border-radius: 4px; padding: 2px 8px; font-size: 12px; color: var(--ink-2); } /* toast — transient feedback (e.g. reload), bottom-left, auto-dismissed */ .toast { position: fixed; bottom: 16px; left: 16px; z-index: 10000; background: var(--elevated); border: 1px solid var(--border); border-radius: 6px; padding: 8px 14px; font-family: var(--mono); font-size: 11px; color: var(--ink-2); box-shadow: 0 4px 18px rgba(0,0,0,0.25); opacity: 0; transform: translateY(10px); pointer-events: none; transition: opacity 0.2s ease, transform 0.2s ease; } .toast.visible { opacity: 1; transform: translateY(0); } .toast.toast-success { border-color: var(--ember); color: var(--ember); } .toast.toast-info { color: var(--ink-2); } /* settings modal — mode segmented control + color theme cards */ .settings-body { padding: 14px 18px 18px; } .settings-section { margin-bottom: 16px; } .settings-section:last-child { margin-bottom: 0; } .settings-label { font-size: 11px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-muted); margin-bottom: 8px; } .seg { display: inline-flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; } .seg button { background: transparent; border: 0; color: var(--ink-muted); font-family: var(--mono); font-size: 12px; padding: 6px 14px; cursor: pointer; transition: all 0.15s ease; } .seg button + button { border-left: 1px solid var(--border); } .seg button:hover { background: var(--hover); color: var(--ink-1); } .seg button.on { color: var(--accent-text); background: var(--ember-dim); } .theme-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } .theme-card { display: flex; align-items: center; gap: 10px; background: var(--field); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; cursor: pointer; text-align: left; font-family: var(--mono); font-size: 12px; color: var(--ink-2); transition: all 0.15s ease; } .theme-card:hover { background: var(--hover); color: var(--ink-1); } .theme-card.on { color: var(--accent-text); border-color: var(--ember); } .swatches { display: inline-flex; gap: 3px; flex: 0 0 auto; } .swatch { width: 12px; height: 12px; border-radius: 50%; border: 1px solid color-mix(in srgb, var(--ink-muted) 40%, transparent); } /* Each card carries both mode previews; show only the resolved mode's dots. (No data-theme attribute = dark default, so hide .sw-light via :not.) */ :root[data-theme="light"] .sw-dark { display: none; } :root:not([data-theme="light"]) .sw-light { display: none; } /* theme gallery — settings shows only starred cards (max 3) + a Browse button; the gallery modal lists every theme with a star toggle per card. */ /* The Browse button is a grid cell: display:contents lifts the starred cards into the parent grid so the button flows inline right after them. */ .starred-cards { display: contents; } /* Match .theme-card's natural height (8px padding + 14px content row — the 12px swatch plus its borders) so the button is the same size whether it shares a row with a card (stretched) or sits on its own row (natural). */ .browse-themes { justify-content: center; padding: 8px 10px; line-height: 14px; } .gallery-scrim { z-index: 60; } /* above the settings scrim (50) — settings stays open */ .modal-wide { width: 560px; max-width: 560px; } .gallery-body { padding: 14px 18px 18px; max-height: 70vh; overflow-y: auto; } /* Star rides the card's right edge; it toggles a favorite without applying the theme (click is swallowed before the card handler). */ .theme-card .fttl { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .theme-star { flex: 0 0 auto; margin-left: auto; background: none; border: 0; padding: 0 2px; font-size: 14px; line-height: 1; color: var(--ink-muted); cursor: pointer; transition: color 0.15s ease; } .theme-star:hover { color: var(--ember-glow); } .theme-star.on { color: var(--ember); } `; export const THEME_CSS = BASE_CSS + colorThemeCss();