globals.css
1 @import 'tailwindcss'; 2 @import 'tw-animate-css'; 3 4 /* Tell Tailwind to scan components and lib at the project root */ 5 @source "../components"; 6 @source "../lib"; 7 8 @custom-variant dark (&:is(.dark *)); 9 10 @theme inline { 11 --color-background: var(--background); 12 --color-foreground: var(--foreground); 13 --font-sans: var(--font-sans); 14 --font-mono: var(--font-mono); 15 --font-serif: var(--font-serif); 16 --font-display: var(--font-display); 17 --color-sidebar-ring: var(--sidebar-ring); 18 --color-sidebar-border: var(--sidebar-border); 19 --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 20 --color-sidebar-accent: var(--sidebar-accent); 21 --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 22 --color-sidebar-primary: var(--sidebar-primary); 23 --color-sidebar-foreground: var(--sidebar-foreground); 24 --color-sidebar: var(--sidebar); 25 --color-chart-5: var(--chart-5); 26 --color-chart-4: var(--chart-4); 27 --color-chart-3: var(--chart-3); 28 --color-chart-2: var(--chart-2); 29 --color-chart-1: var(--chart-1); 30 --color-ring: var(--ring); 31 --color-input: var(--input); 32 --color-border: var(--border); 33 --color-destructive: var(--destructive); 34 --color-accent-foreground: var(--accent-foreground); 35 --color-accent: var(--accent); 36 --color-muted-foreground: var(--muted-foreground); 37 --color-muted: var(--muted); 38 --color-secondary-foreground: var(--secondary-foreground); 39 --color-secondary: var(--secondary); 40 --color-primary-foreground: var(--primary-foreground); 41 --color-primary: var(--primary); 42 --color-popover-foreground: var(--popover-foreground); 43 --color-popover: var(--popover); 44 --color-card-foreground: var(--card-foreground); 45 --color-card: var(--card); 46 --radius-sm: calc(var(--radius) - 4px); 47 --radius-md: calc(var(--radius) - 2px); 48 --radius-lg: var(--radius); 49 --radius-xl: calc(var(--radius) + 4px); 50 --radius-2xl: calc(var(--radius) + 8px); 51 --radius-3xl: calc(var(--radius) + 12px); 52 --radius-4xl: calc(var(--radius) + 16px); 53 } 54 55 :root { 56 /* ─── Type system ──────────────────────────────────────────── 57 Editorial monograph stack: 58 - Plus Jakarta Sans for UI and prose (refined, slightly warmer than Inter) 59 - Fraunces variable for slide titles, chapter labels, display roles 60 (opsz axis tuned per role; carries the brand) 61 - JetBrains Mono for code and quiet metadata 62 The legacy --font-display token is preserved but now points at the 63 editorial serif so existing `font-display` Tailwind utilities resolve 64 to the serif rather than the old Sora sans. */ 65 --font-sans: 66 'Plus Jakarta Sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 67 --font-serif: 'Fraunces', 'Source Serif 4', 'Iowan Old Style', 'Charter', Georgia, 'Times New Roman', serif; 68 --font-display: var(--font-serif); 69 --font-mono: 'JetBrains Mono', ui-monospace, 'Cascadia Code', 'Fira Code', monospace; 70 71 /* Reading column width — 65–75ch is the editorial sweet spot. 72 Used by .slide-prose and .editorial-measure helper classes. */ 73 --measure-prose: 68ch; 74 --measure-prose-narrow: 56ch; 75 76 /* ─── Semantic status colors ────────────────────────────────── 77 These tokens are used by Badge components, diff stats, and 78 status indicators. They provide a single source of truth so 79 components don't hardcode oklch values inline. The full-opacity 80 versions are for text; the /35% border and /10% bg variants 81 are computed at usage site via Tailwind arbitrary values. */ 82 --color-success: oklch(0.42 0.11 145); /* green — additions, approved, open */ 83 --color-danger: oklch(0.45 0.18 22); /* red — deletions, closed, critical */ 84 --color-warning: oklch(0.45 0.14 55); /* amber — outdated, churn */ 85 --color-info: oklch(0.45 0.1 300); /* purple — merged */ 86 87 --radius: 0.625rem; 88 89 /* ─── Editorial palette — light mode (printed paper) ───────── 90 Neutrals are tinted toward hue 65 (warm gray) at very low 91 chroma so they read as warm, never cool. The brand accent 92 (--ring) is now a deep CLARET — the color of academic press 93 spines, fountain-pen ink, and leather-bound book covers. 94 It's the only chromatic moment in the UI; buttons and primary 95 surfaces are near-black ink on warm paper. */ 96 --background: oklch(0.985 0.005 75); 97 --foreground: oklch(0.2 0.008 65); 98 /* Cards barely exist — same color as the page. */ 99 --card: oklch(0.985 0.005 75); 100 --card-foreground: oklch(0.2 0.008 65); 101 --popover: oklch(0.99 0.005 75); 102 --popover-foreground: oklch(0.2 0.008 65); 103 /* --primary is ink, not the brand accent. Filled buttons are 104 near-black on paper, like a printed page. */ 105 --primary: oklch(0.2 0.008 65); 106 --primary-foreground: oklch(0.985 0.005 75); 107 --secondary: oklch(0.95 0.006 70); 108 --secondary-foreground: oklch(0.2 0.008 65); 109 --muted: oklch(0.95 0.006 70); 110 --muted-foreground: oklch(0.45 0.012 65); 111 --accent: oklch(0.95 0.006 70); 112 --accent-foreground: oklch(0.2 0.008 65); 113 --destructive: oklch(0.55 0.16 25); 114 --border: oklch(0.9 0.006 65); 115 --input: oklch(0.9 0.006 65); 116 /* --ring IS the brand accent — deep claret / oxblood. Used as 117 the focus indicator AND as the single recurring chromatic 118 punctuation across the app (active slide marker, link color, 119 comment affordance, brand bullet). Hue 15, deeper value, much 120 lower chroma than the old warm-amber accent. */ 121 --ring: oklch(0.4 0.12 15); 122 /* Chart-1 mirrors the brand for diagrams; the rest of the chart 123 palette stays distinct so multi-series Mermaid still reads. */ 124 --chart-1: oklch(0.4 0.12 15); 125 --chart-2: oklch(0.5 0.1 145); 126 --chart-3: oklch(0.45 0.1 240); 127 --chart-4: oklch(0.55 0.12 80); 128 --chart-5: oklch(0.5 0.1 280); 129 --sidebar: oklch(0.985 0.005 75); 130 --sidebar-foreground: oklch(0.2 0.008 65); 131 --sidebar-primary: oklch(0.2 0.008 65); 132 --sidebar-primary-foreground: oklch(0.985 0.005 75); 133 --sidebar-accent: oklch(0.95 0.006 70); 134 --sidebar-accent-foreground: oklch(0.2 0.008 65); 135 --sidebar-border: oklch(0.9 0.006 65); 136 --sidebar-ring: oklch(0.4 0.12 15); 137 } 138 139 .dark { 140 /* ─── Editorial palette — dark mode (well-lit study at night) ─ 141 Warm charcoal background (NOT near-black), warm off-white 142 foreground. Cards are barely lighter than the page. The 143 brand claret lifts in dark mode so it stays visible against 144 the deeper background without becoming a glow. */ 145 --background: oklch(0.155 0.006 65); 146 --foreground: oklch(0.965 0.005 75); 147 --card: oklch(0.165 0.006 65); 148 --card-foreground: oklch(0.965 0.005 75); 149 --popover: oklch(0.18 0.006 65); 150 --popover-foreground: oklch(0.965 0.005 75); 151 /* In dark mode --primary inverts to paper, so filled buttons 152 are off-white on warm charcoal — same editorial logic as 153 light mode, just inverted. */ 154 --primary: oklch(0.965 0.005 75); 155 --primary-foreground: oklch(0.18 0.008 65); 156 --secondary: oklch(0.24 0.008 65); 157 --secondary-foreground: oklch(0.965 0.005 75); 158 --muted: oklch(0.24 0.008 65); 159 --muted-foreground: oklch(0.68 0.012 65); 160 --accent: oklch(0.24 0.008 65); 161 --accent-foreground: oklch(0.965 0.005 75); 162 --destructive: oklch(0.66 0.16 25); 163 --border: oklch(0.965 0.005 75 / 9%); 164 --input: oklch(0.965 0.005 75 / 13%); 165 /* Brand claret, lifted for dark-mode legibility. Stays low 166 enough chroma that it doesn't read as "glow." */ 167 --ring: oklch(0.65 0.13 15); 168 --chart-1: oklch(0.65 0.13 15); 169 --chart-2: oklch(0.62 0.1 145); 170 --chart-3: oklch(0.6 0.11 240); 171 --chart-4: oklch(0.7 0.12 80); 172 --chart-5: oklch(0.62 0.1 280); 173 --sidebar: oklch(0.165 0.006 65); 174 --sidebar-foreground: oklch(0.965 0.005 75); 175 --sidebar-primary: oklch(0.965 0.005 75); 176 --sidebar-primary-foreground: oklch(0.18 0.008 65); 177 --sidebar-accent: oklch(0.24 0.008 65); 178 --sidebar-accent-foreground: oklch(0.965 0.005 75); 179 --sidebar-border: oklch(0.965 0.005 75 / 9%); 180 --sidebar-ring: oklch(0.65 0.13 15); 181 182 --color-success: oklch(0.72 0.1 145); 183 --color-danger: oklch(0.68 0.16 22); 184 --color-warning: oklch(0.72 0.12 55); 185 --color-info: oklch(0.65 0.1 300); 186 } 187 188 @layer base { 189 * { 190 @apply border-border outline-ring/50; 191 scrollbar-color: oklch(0.45 0.01 65) transparent; 192 -webkit-tap-highlight-color: transparent; 193 -webkit-touch-callout: none; 194 } 195 196 html, 197 body { 198 overscroll-behavior: none; 199 overflow: hidden; 200 } 201 202 body { 203 @apply bg-background text-foreground; 204 font-family: var(--font-sans); 205 font-size: 1rem; 206 line-height: 1.55; 207 font-kerning: normal; 208 font-feature-settings: 209 'kern' 1, 210 'liga' 1, 211 'calt' 1, 212 'ss01' 1; 213 -webkit-user-select: none; 214 user-select: none; 215 -webkit-font-smoothing: antialiased; 216 -moz-osx-font-smoothing: grayscale; 217 text-rendering: optimizeLegibility; 218 -webkit-text-size-adjust: 100%; 219 } 220 221 /* Tabular numerals for any element that should align numerically 222 (commit counts, line counts, time durations, sizes). */ 223 .tabular, 224 time, 225 [data-tabular] { 226 font-variant-numeric: tabular-nums; 227 } 228 229 input, 230 textarea, 231 [contenteditable], 232 .selectable { 233 -webkit-user-select: text; 234 user-select: text; 235 } 236 237 label, 238 button, 239 [role='button'], 240 span, 241 p, 242 h1, 243 h2, 244 h3, 245 h4, 246 h5, 247 h6, 248 .ui-label { 249 cursor: default; 250 } 251 252 input, 253 textarea, 254 [contenteditable] { 255 cursor: text; 256 } 257 258 a, 259 button, 260 summary, 261 [role='button'], 262 .clickable { 263 cursor: pointer; 264 } 265 266 img, 267 a, 268 svg { 269 -webkit-user-drag: none; 270 } 271 272 button:focus:not(:focus-visible), 273 [role='button']:focus:not(:focus-visible) { 274 outline: none; 275 } 276 277 /* Unified focus ring across every focusable element. The warm 278 amber `--ring` is the brand accent and contrasts cleanly 279 against both the paper and warm-charcoal backgrounds. */ 280 button:focus-visible, 281 [role='button']:focus-visible, 282 a:focus-visible, 283 [tabindex]:focus-visible, 284 summary:focus-visible { 285 outline: 2px solid var(--ring); 286 outline-offset: 2px; 287 border-radius: 2px; 288 } 289 290 /* Inputs use a border-color shift instead of an outline so the 291 bottom-bordered editorial input style doesn't get a doubled 292 ring on focus. */ 293 input:focus-visible, 294 textarea:focus-visible, 295 select:focus-visible { 296 outline: none; 297 border-color: var(--ring); 298 } 299 } 300 301 /* ─── Editorial typography ───────────────────────────────────── */ 302 303 /* Slide title — the single highest-leverage typographic element. 304 Editorial serif (Fraunces), display optical size, modest weight, 305 tight line-height. Fluid clamp() because slide titles are display 306 content on a content-dominated layout. */ 307 .slide-title { 308 font-family: var(--font-serif); 309 font-size: clamp(1.75rem, 0.9rem + 2.4vw, 2.625rem); /* 28→42px */ 310 font-weight: 440; 311 line-height: 1.12; 312 letter-spacing: -0.014em; 313 font-feature-settings: 314 'kern' 1, 315 'liga' 1, 316 'calt' 1, 317 'ss01' 1; 318 /* Fraunces opsz=72 keeps the display character at 28-42px without 319 getting as thin and elegant as opsz=96 (which is designed for 320 billboard sizes and reads as fragile in product UI). */ 321 font-variation-settings: 'opsz' 72; 322 color: var(--foreground); 323 text-wrap: balance; 324 } 325 326 /* Smaller serif heading used inside narrative blocks (callouts, 327 sub-sections, dialog titles that should still feel editorial). 328 Weight 540 — clearly heavier than slide-title's 440 so the 329 hierarchy reads at a glance. The previous 460 was only 40 units 330 off the display title and barely registered as different. */ 331 .editorial-heading { 332 font-family: var(--font-serif); 333 font-size: 1.375rem; /* 22px */ 334 font-weight: 540; 335 line-height: 1.25; 336 letter-spacing: -0.008em; 337 font-variation-settings: 'opsz' 36; 338 color: var(--foreground); 339 text-wrap: balance; 340 } 341 342 /* Chapter chip that sits ABOVE the slide title. 343 Quiet mono in the warm-gray register, exactly like the 344 "Section 01 · 1 file" label in the reference screenshot. */ 345 .slide-chapter { 346 display: inline-flex; 347 align-items: baseline; 348 gap: 0.5ch; 349 font-family: var(--font-mono); 350 font-size: 0.75rem; /* 12px */ 351 font-weight: 500; 352 letter-spacing: 0.04em; 353 line-height: 1; 354 color: var(--muted-foreground); 355 font-variant-numeric: tabular-nums; 356 } 357 358 /* Prose column for slide narrative. 359 Comfortable measure, generous leading, real paragraph spacing. 360 Body color is `--foreground` (NOT muted) — the reading column 361 is the user's primary content, not a footnote. Setting it to 362 muted made every long passage feel like a caption. 363 `text-wrap: pretty` is the modern CSS feature that optimizes 364 the last few lines of every paragraph to avoid orphans and 365 ragged shapes — the one thing justification was reaching for, 366 without any of justification's drawbacks. Falls back silently 367 in browsers that don't support it. */ 368 .slide-prose { 369 max-width: var(--measure-prose); 370 font-family: var(--font-sans); 371 font-size: 1rem; /* 16px — never below */ 372 line-height: 1.65; 373 color: var(--foreground); 374 text-wrap: pretty; 375 } 376 377 .slide-prose p + p { 378 margin-top: 0.85em; 379 } 380 381 /* Strong was already at foreground 600; once the surrounding text 382 moved to foreground too, the contrast comes from weight alone 383 instead of weight + color. That's the more honest move and reads 384 as cleaner emphasis. */ 385 .slide-prose strong { 386 font-weight: 600; 387 } 388 389 .slide-prose em { 390 font-style: italic; 391 } 392 393 /* Per-element styles for code, links, lists, blockquote, and 394 headings inside slide-prose all live in the Markdown component 395 (components/Markdown.tsx) so the editorial register applies 396 consistently in both .slide-prose contexts (narrative, summary) 397 AND non-slide-prose contexts (chat messages, PR description, 398 context snippets). Putting them here would shadow the Markdown 399 per-element classes via lower specificity. */ 400 401 /* Quiet typographic meta line — used for the status row in the 402 reference screenshot ("8/8 files narrated · all files covered"), 403 for affected-files lists, and any other margin-note copy. */ 404 .slide-meta { 405 font-family: var(--font-mono); 406 font-size: 0.75rem; /* 12px */ 407 line-height: 1.5; 408 letter-spacing: 0.01em; 409 color: var(--muted-foreground); 410 font-variant-numeric: tabular-nums; 411 } 412 413 /* Inline editorial label — bold sentence-case run-in heading. 414 Used for callouts that should read like a sidebar in a printed 415 essay rather than a "tip card". */ 416 .editorial-label { 417 font-family: var(--font-sans); 418 font-weight: 600; 419 color: var(--foreground); 420 } 421 422 /* Editorial callout block — the "What to check" / "Note." pattern 423 from the brief: a soft tinted block with a bold inline label 424 followed by prose on the same line. NO side-stripe border, NO 425 icon, NO titled card chrome. The tint is the warm-paper variant 426 of the brand claret at very low alpha so it reads as a margin 427 note, not as an alert. Both modes tested for AA contrast. */ 428 .editorial-callout { 429 background: oklch(0.4 0.12 15 / 5%); 430 border: 1px solid oklch(0.4 0.12 15 / 16%); 431 border-radius: 4px; 432 padding: 0.85rem 1.1rem; 433 } 434 435 .dark .editorial-callout { 436 background: oklch(0.65 0.13 15 / 8%); 437 border-color: oklch(0.65 0.13 15 / 22%); 438 } 439 440 /* Pending review comment block — the user's draft comment as it 441 appears under a diff line. Tinted in the brand claret so it 442 reads as "this is your work, not the diff itself." Both modes 443 tested for AA contrast on the text. */ 444 .pending-comment-block { 445 background: oklch(0.4 0.12 15 / 7%); 446 border-color: oklch(0.4 0.12 15 / 30%); 447 } 448 449 .pending-comment-block-text { 450 color: oklch(0.42 0.13 15); 451 } 452 453 .dark .pending-comment-block { 454 background: oklch(0.65 0.13 15 / 10%); 455 border-color: oklch(0.65 0.13 15 / 36%); 456 } 457 458 .dark .pending-comment-block-text { 459 color: oklch(0.78 0.13 15); 460 } 461 462 /* Keyboard key — small mono caps on a hairline-bordered chip. 463 Used in the shortcut cheatsheet and inline help text. The 464 light-mode fill stays subtle (6% on warm gray); dark mode 465 lifts to 12% so the chip has visible weight against the 466 warm-charcoal background. */ 467 .kbd { 468 display: inline-flex; 469 align-items: center; 470 justify-content: center; 471 min-width: 1.5rem; 472 padding: 0.125rem 0.4rem; 473 font-family: var(--font-mono); 474 font-size: 0.6875rem; 475 line-height: 1.2; 476 font-weight: 500; 477 color: var(--foreground); 478 background: oklch(0.55 0.012 65 / 6%); 479 border: 1px solid oklch(0.55 0.012 65 / 32%); 480 border-radius: 3px; 481 letter-spacing: 0.02em; 482 white-space: nowrap; 483 } 484 485 .dark .kbd { 486 background: oklch(0.7 0.012 65 / 12%); 487 border-color: oklch(0.7 0.012 65 / 32%); 488 } 489 490 /* ─── Dark scrollbars (WebKit) ───────────────────────────────── */ 491 ::-webkit-scrollbar { 492 width: 8px; 493 height: 8px; 494 } 495 496 ::-webkit-scrollbar-track { 497 background: transparent; 498 } 499 500 ::-webkit-scrollbar-thumb { 501 background: oklch(0.45 0.01 65 / 60%); 502 border-radius: 4px; 503 } 504 505 ::-webkit-scrollbar-thumb:hover { 506 background: oklch(0.55 0.012 65 / 80%); 507 } 508 509 ::-webkit-scrollbar-corner { 510 background: transparent; 511 } 512 513 /* ─── Shiki base ─────────────────────────────────────────────── */ 514 .shiki { 515 font-family: var(--font-mono) !important; 516 overflow-x: auto; 517 padding: 0.5rem 0 !important; 518 } 519 520 /* Shiki renders \n text nodes between <span class="line"> elements. 521 Setting font-size:0 here collapses those text nodes to zero height 522 so they don't create extra blank lines between code lines. 523 font-size is restored on .line. */ 524 .shiki code { 525 display: block; 526 font-size: 0; 527 /* wide enough that backgrounds fill the full scrollable area */ 528 min-width: 100%; 529 width: max-content; 530 } 531 532 .shiki .line { 533 display: block; 534 font-size: 0.8125rem; 535 line-height: 1.5; 536 padding: 0 1.25rem; 537 } 538 539 /* ─── Diff gutter (only inside has-diff blocks) ──────────────── */ 540 541 /* All lines in a diff block get a fixed-width gutter column so 542 indentation stays perfectly aligned between +, - and context. */ 543 .has-diff .line { 544 padding-left: 0; 545 } 546 547 .has-diff .line::before { 548 /* 1ch = one character width in the current monospace font */ 549 display: inline-block; 550 width: 1ch; 551 margin-right: 1ch; /* one char gap between gutter and code */ 552 padding-left: 0.75rem; 553 content: ' '; 554 user-select: none; 555 pointer-events: none; 556 } 557 558 /* Added lines */ 559 .has-diff .line.diff.add { 560 background-color: rgba(46, 160, 67, 0.15); 561 } 562 563 .has-diff .line.diff.add::before { 564 content: '+'; 565 color: #3fb950; 566 } 567 568 /* Removed lines */ 569 .has-diff .line.diff.remove { 570 background-color: rgba(248, 81, 73, 0.15); 571 } 572 573 .has-diff .line.diff.remove::before { 574 content: '-'; 575 color: #f85149; 576 } 577 578 /* ─── Interactive diff lines ─────────────────────────────────── */ 579 580 /* Override the default gutter for interactive lines since we render our own */ 581 .has-diff .line.interactive-line::before { 582 display: none; 583 } 584 585 .line.interactive-line:hover .add-comment-icon { 586 opacity: 1 !important; 587 } 588 589 .line.interactive-line:hover .line-number-gutter { 590 color: var(--foreground) !important; 591 } 592 593 .line.interactive-line { 594 padding-left: 0 !important; 595 } 596 597 /* ─── Split diff view ─────────────────────────────────────────── */ 598 599 .split-diff-cell-add { 600 background-color: rgba(46, 160, 67, 0.15); 601 } 602 603 .split-diff-cell-remove { 604 background-color: rgba(248, 81, 73, 0.15); 605 } 606 607 .split-diff-cell-empty { 608 background: oklch(0.5 0.012 65 / 8%); 609 } 610 611 .split-diff-separator { 612 background-color: oklch(0.5 0.012 65 / 12%); 613 } 614 615 .split-diff-grid > span.split-diff-line-num:hover { 616 color: var(--foreground) !important; 617 } 618 619 .split-diff-grid > span.split-diff-comment-icon:hover .split-icon-hover { 620 opacity: 1 !important; 621 } 622 623 /* ─── Check suggestion jump highlight ────────────────────────── */ 624 625 .check-highlight { 626 position: relative; 627 } 628 .check-highlight::after { 629 content: ''; 630 position: absolute; 631 inset: 0; 632 pointer-events: none; 633 /* Claret flash — same hue as the brand --ring, just briefly 634 visible to show the user where the check landed. */ 635 background-color: oklch(0.55 0.14 15 / 22%); 636 animation: check-highlight-fade 1.5s ease-out forwards; 637 } 638 @keyframes check-highlight-fade { 639 from { 640 opacity: 1; 641 } 642 to { 643 opacity: 0; 644 } 645 } 646 647 /* ─── Freshness banner ──────────────────────────────────────── 648 Quieted to fit the editorial register. Warmer hues, lower 649 chroma, and very subtle backgrounds — these banners are 650 informational, not attention-grabbing. Light-mode text drops 651 to L≈0.42 to hit WCAG AA against the paper background; dark 652 mode lifts back to L≈0.7. */ 653 654 .staleBanner-current { 655 border: 1px solid oklch(0.5 0.1 145 / 38%); 656 background: oklch(0.5 0.1 145 / 8%); 657 color: oklch(0.42 0.1 145); 658 } 659 660 .staleBanner-unknown { 661 border: 1px solid oklch(0.45 0.012 65 / 32%); 662 background: oklch(0.45 0.012 65 / 6%); 663 color: var(--muted-foreground); 664 } 665 666 .staleBanner-warn { 667 border: 1px solid oklch(0.55 0.13 55 / 38%); 668 background: oklch(0.55 0.13 55 / 8%); 669 color: oklch(0.42 0.14 55); 670 } 671 672 .staleBanner-warn-toggle:hover { 673 color: oklch(0.32 0.14 55); 674 } 675 676 .staleBanner-warn-btn { 677 border: 1px solid oklch(0.55 0.13 55 / 38%); 678 background: oklch(0.55 0.13 55 / 10%); 679 color: oklch(0.42 0.14 55); 680 } 681 682 .staleBanner-warn-btn:hover { 683 background: oklch(0.55 0.13 55 / 16%); 684 } 685 686 .dark .staleBanner-current { 687 border-color: oklch(0.65 0.1 145 / 36%); 688 background: oklch(0.65 0.1 145 / 10%); 689 color: oklch(0.72 0.1 145); 690 } 691 692 .dark .staleBanner-warn { 693 border-color: oklch(0.7 0.13 55 / 36%); 694 background: oklch(0.7 0.13 55 / 10%); 695 color: oklch(0.78 0.13 55); 696 } 697 698 .dark .staleBanner-warn-toggle:hover { 699 color: oklch(0.85 0.13 55); 700 } 701 702 .dark .staleBanner-warn-btn { 703 border-color: oklch(0.7 0.13 55 / 36%); 704 background: oklch(0.7 0.13 55 / 10%); 705 color: oklch(0.82 0.13 55); 706 } 707 708 .dark .staleBanner-warn-btn:hover { 709 background: oklch(0.7 0.13 55 / 18%); 710 } 711 712 .staleBanner-warn-list { 713 border-top: 1px solid oklch(0.6 0.1 55 / 16%); 714 } 715 716 .staleBanner-warn-sha { 717 color: oklch(0.65 0.08 55 / 75%); 718 font-family: var(--font-mono); 719 } 720 721 .staleBanner-warn-meta { 722 color: oklch(0.65 0.08 55 / 55%); 723 } 724 725 /* ─── Update banner ────────────────────────────────────────── 726 Warm umber instead of cool blue, to match the editorial 727 palette. Used for "a new version is available". */ 728 729 .updateBanner { 730 border-bottom: 1px solid oklch(0.55 0.11 80 / 38%); 731 background: oklch(0.55 0.11 80 / 8%); 732 color: oklch(0.42 0.12 80); 733 } 734 735 .updateBanner-btn { 736 border: 1px solid oklch(0.55 0.11 80 / 38%); 737 background: oklch(0.55 0.11 80 / 10%); 738 color: oklch(0.42 0.12 80); 739 } 740 741 .updateBanner-btn:hover { 742 background: oklch(0.55 0.11 80 / 18%); 743 } 744 745 .dark .updateBanner { 746 border-color: oklch(0.7 0.12 80 / 36%); 747 background: oklch(0.7 0.12 80 / 10%); 748 color: oklch(0.78 0.12 80); 749 } 750 751 .dark .updateBanner-btn { 752 border-color: oklch(0.7 0.12 80 / 36%); 753 background: oklch(0.7 0.12 80 / 10%); 754 color: oklch(0.82 0.12 80); 755 } 756 757 .dark .updateBanner-btn:hover { 758 background: oklch(0.7 0.12 80 / 18%); 759 } 760 761 /* ─── PR status pills ─────────────────────────────────────────── */ 762 763 .statusPill-green, 764 .statusPill-red, 765 .statusPill-amber, 766 .statusPill-neutral, 767 .statusPill-label { 768 display: inline-flex; 769 align-items: center; 770 gap: 0.375rem; 771 padding: 0.25rem 0.625rem; 772 border-radius: 9999px; 773 font-size: 0.75rem; 774 font-weight: 500; 775 line-height: 1; 776 white-space: nowrap; 777 } 778 779 /* All status pills are quieted: lower chroma, very subtle fills, 780 warmer hues. They're informational — they should NEVER be the 781 loudest thing on a screen. /distill collapsed most of them into 782 a single mono line; remaining usages (PR state badges, the 783 slide chat counter, the experimental tag) sit calmly. 784 785 Light-mode text colors are dropped to L≈0.42 so they hit WCAG 786 AA contrast (≥4.5:1) against the paper background. Dark mode 787 overrides lift them back to L≈0.62–0.7 so they stay readable 788 on the warm-charcoal background. */ 789 790 .statusPill-green { 791 border: 1px solid oklch(0.5 0.1 145 / 38%); 792 background: oklch(0.5 0.1 145 / 8%); 793 color: oklch(0.42 0.11 145); 794 } 795 796 .statusPill-red { 797 border: 1px solid oklch(0.55 0.16 22 / 38%); 798 background: oklch(0.55 0.16 22 / 8%); 799 color: oklch(0.45 0.18 22); 800 } 801 802 .statusPill-amber { 803 border: 1px solid oklch(0.6 0.13 55 / 38%); 804 background: oklch(0.6 0.13 55 / 8%); 805 color: oklch(0.45 0.14 55); 806 } 807 808 .statusPill-neutral { 809 border: 1px solid oklch(0.45 0.012 65 / 32%); 810 background: oklch(0.45 0.012 65 / 6%); 811 color: var(--muted-foreground); 812 } 813 814 .statusPill-label { 815 /* Was cool blue; now matches the warm neutral so it sits 816 quietly with the rest of the chrome. */ 817 border: 1px solid oklch(0.45 0.012 65 / 32%); 818 background: oklch(0.45 0.012 65 / 6%); 819 color: var(--muted-foreground); 820 } 821 822 .statusPill-skeleton { 823 border: 1px solid oklch(0.45 0.012 65 / 22%); 824 background: oklch(0.45 0.012 65 / 8%); 825 } 826 827 /* Dark-mode overrides — lift the text back up so it reads against 828 the warm-charcoal background. Borders also need slightly more 829 alpha to remain visible. */ 830 .dark .statusPill-green { 831 border-color: oklch(0.65 0.1 145 / 36%); 832 background: oklch(0.65 0.1 145 / 10%); 833 color: oklch(0.72 0.1 145); 834 } 835 836 .dark .statusPill-red { 837 border-color: oklch(0.7 0.16 22 / 38%); 838 background: oklch(0.7 0.16 22 / 10%); 839 color: oklch(0.75 0.16 22); 840 } 841 842 .dark .statusPill-amber { 843 border-color: oklch(0.72 0.13 55 / 38%); 844 background: oklch(0.72 0.13 55 / 10%); 845 color: oklch(0.78 0.13 55); 846 } 847 848 .dark .statusPill-neutral, 849 .dark .statusPill-label { 850 border-color: oklch(0.65 0.012 65 / 32%); 851 background: oklch(0.65 0.012 65 / 8%); 852 } 853 854 /* ─── Review focus callout ────────────────────────────────────── */ 855 856 .review-focus-content ul, 857 ul.review-focus-content { 858 list-style: none; 859 padding-left: 0; 860 } 861 862 .review-focus-content ul > li, 863 ul.review-focus-content > li { 864 position: relative; 865 padding-left: 1.5rem; 866 } 867 868 .review-focus-content ul > li::before, 869 ul.review-focus-content > li::before { 870 content: '▸'; 871 position: absolute; 872 left: 0; 873 /* Warm amber brand accent — the bullet IS the chromatic 874 punctuation. Used sparingly so it actually means something. */ 875 color: var(--ring); 876 opacity: 0.85; 877 font-size: 0.875rem; 878 line-height: inherit; 879 } 880 881 /* ─── Mermaid fullscreen dialog ───────────────────────────────── */ 882 883 .mermaid-fullscreen svg { 884 width: auto !important; 885 max-width: 100% !important; 886 height: auto !important; 887 max-height: calc(90vh - 6rem); 888 } 889 890 /* ─── Slide transitions (Priority 3) ─────────────────────────── */ 891 892 @keyframes slide-enter { 893 from { 894 opacity: 0; 895 transform: translateY(6px); 896 } 897 to { 898 opacity: 1; 899 transform: translateY(0); 900 } 901 } 902 903 .slide-enter { 904 animation: slide-enter 0.25s cubic-bezier(0.22, 1, 0.36, 1); 905 } 906 907 /* ─── Entrance animations (Priority 5) ────────────────────────── */ 908 909 @keyframes fade-in-up { 910 from { 911 opacity: 0; 912 transform: translateY(8px); 913 } 914 to { 915 opacity: 1; 916 transform: translateY(0); 917 } 918 } 919 920 .animate-fade-in-up { 921 animation: fade-in-up 0.35s cubic-bezier(0.22, 1, 0.36, 1) both; 922 } 923 924 /* Brief fade-in for elements that appear in response to a user 925 action (review checkmark, quoted-code preview, status changes). 926 150ms is fast enough to feel responsive, slow enough to register 927 as "something appeared" rather than "it was always there." */ 928 @keyframes fade-in { 929 from { opacity: 0; } 930 to { opacity: 1; } 931 } 932 933 .animate-fade-in { 934 animation: fade-in 150ms cubic-bezier(0.22, 1, 0.36, 1) both; 935 } 936 937 /* ─── Loading screen ───────────────────────────────────────── 938 Replaces the old pulsing-ring + cyan-glow loader with a quiet 939 type-led page. The wait is the first chapter of the monograph, 940 not a separate "loading state". */ 941 942 /* Streaming text panel — calm mono on a faint warm tint. 943 No box-shadow, no glow, no colored border. `overflow-wrap` 944 handles long URLs gracefully (the old `break-all` mangled 945 them character-by-character). */ 946 .loading-stream { 947 background: oklch(0.65 0.012 65 / 5%); 948 border: 1px solid oklch(0.55 0.012 65 / 18%); 949 overflow-wrap: anywhere; 950 word-break: normal; 951 line-height: 1.55; 952 } 953 954 /* Progress fill transitions smoothly between phases. The 955 transition is the only motion on the loading screen apart 956 from the optional fade-in on the title. */ 957 .loading-progress-fill { 958 transition: width 600ms cubic-bezier(0.22, 1, 0.36, 1); 959 } 960 961 /* ─── Reduced motion ───────────────────────────────────────── 962 Honor the OS preference. Every transition and animation in 963 the app collapses to instantaneous changes for users who 964 asked for it. The progress bar still updates — it just 965 snaps instead of sliding. */ 966 @media (prefers-reduced-motion: reduce) { 967 *, 968 *::before, 969 *::after { 970 animation-duration: 0.001ms !important; 971 animation-iteration-count: 1 !important; 972 transition-duration: 0.001ms !important; 973 scroll-behavior: auto !important; 974 } 975 }