/ src / globals.css
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  }