/**
 * Library - David Steinberg's Original Essays and Book Chapters
 * 
 * A curated collection of investment writings organized by category,
 * with reading view, download capabilities, and progress tracking.
 */

import { useState, useEffect, useRef, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Input } from "@/components/ui/input";
import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
import { 
  ArrowLeft,
  BookOpen,
  Download,
  Search,
  ChevronRight,
  Clock,
  FileText,
  Bookmark,
  X,
  ExternalLink,
  Volume2,
  Loader2,
  CheckCircle2,
  Circle,
  Trophy
} from "lucide-react";
import { Link } from "wouter";
import { motion, AnimatePresence } from "framer-motion";
import { LazyStreamdown as Streamdown } from "@/components/LazyStreamdown";
import { trpc } from "@/lib/trpc";
import { useAuth } from "@/_core/hooks/useAuth";
import { AudioPlayer, VoiceSelector } from "@/components/AudioPlayer";
import { ElevenLabsListenButton } from "@/components/ElevenLabsListenButton";
import { BookmarkPopup } from "@/components/BookmarkPopup";
import { BookmarksPanel } from "@/components/BookmarksPanel";
import { errorTranslators } from "@/lib/errorMessages";
import { toast } from "sonner";
import { LibraryGridSkeleton } from "@/components/SkeletonLoaders";
import { SEOHead, pageSEO } from "@/components/SEOHead";
import { BreadcrumbJsonLd, breadcrumbs } from "@/components/BreadcrumbJsonLd";
import { PageTransition } from "@/components/PageTransition";
import { Breadcrumb } from "@/components/Breadcrumb";
import { formatReadingTime, getReadingStats } from "@/lib/readingTime";
import { QUERY_CACHE } from "@/lib/queryConfig";
import { EmptyState, ErrorState } from "@/components/EmptyState";
import { ScrollReveal } from "@/components/ScrollReveal";

// Types
interface Essay {
  id: string;
  title: string;
  category: string;
  description: string;
  readTime: string;
  content: string;
  source?: string;
}

// Categories
const categories = [
  { id: "all", name: "All Essays", icon: BookOpen },
  { id: "philosophy", name: "Investment Philosophy", icon: BookOpen },
  { id: "risk", name: "Risk Management", icon: BookOpen },
  { id: "psychology", name: "Market Psychology", icon: BookOpen },
  { id: "history", name: "Market History", icon: BookOpen },
  { id: "process", name: "Investment Process", icon: BookOpen },
  { id: "culture", name: "Marlowe Culture", icon: BookOpen },
  { id: "operations", name: "Business Operations", icon: BookOpen },
];

// ─── Scroll Tracker Component ────────────────────────────────────────────────

function LibraryScrollTracker({
  scrollRef,
  essayId,
  isAuthenticated,
  onScrollChange,
  lastSavedRef,
  updateMutation,
}: {
  scrollRef: React.RefObject<HTMLDivElement | null>;
  essayId: string;
  isAuthenticated: boolean;
  onScrollChange: (percent: number) => void;
  lastSavedRef: React.MutableRefObject<number>;
  updateMutation: { mutate: (data: { essayId: string; scrollProgress: number }) => void };
}) {
  useEffect(() => {
    const viewport = scrollRef.current?.querySelector('[data-slot="scroll-area-viewport"]') as HTMLElement | null;
    if (!viewport) return;

    let saveTimer: ReturnType<typeof setTimeout> | null = null;

    const handleScroll = () => {
      const { scrollTop, scrollHeight, clientHeight } = viewport;
      const maxScroll = scrollHeight - clientHeight;
      if (maxScroll <= 0) return;

      const percent = Math.round((scrollTop / maxScroll) * 100);
      onScrollChange(percent);

      // Debounced save: only save if authenticated and progress increased by 5%+
      if (isAuthenticated && percent - lastSavedRef.current >= 5) {
        if (saveTimer) clearTimeout(saveTimer);
        saveTimer = setTimeout(() => {
          lastSavedRef.current = percent;
          updateMutation.mutate({
            essayId,
            scrollProgress: percent,
          });
        }, 1000);
      }
    };

    viewport.addEventListener('scroll', handleScroll, { passive: true });
    return () => {
      viewport.removeEventListener('scroll', handleScroll);
      if (saveTimer) clearTimeout(saveTimer);
    };
  }, [essayId, isAuthenticated]);

  return null;
}

// Essays from David Steinberg's books
const essays: Essay[] = [
  // Investment Philosophy
  {
    id: "why-stock-market",
    title: "Why Do We Have a Stock Market?",
    category: "philosophy",
    description: "Understanding the origins and purpose of equity markets, from mercantilism to modern investing.",
    readTime: "8 min",
    source: "The Ocean Doesn't Care About Your Swimming Lessons",
    content: `# Why Do We Have a Stock Market and Where Did It Come From?

What is the stock market? This might sound simplistic, but without intuition about the markets and where they came from, it is difficult to have intuition about Marlowe's perspective on investing. My grandfather was a limitless source of common sense. One proverb of my childhood: two young fish are swimming in the ocean when an older fish passes by and says, "enjoy the warm water today." The two younger fish look at each other in confusion and ask, "what the heck is water?" If we are going to swim together, it is worthwhile to know something about the environment that I've spent my adult life studying.

The stock market as we imagine it today is young, perhaps 400 to 500 years old. Prior to that, the world operated in a mercantilist system. Commercial opportunities didn't need a stock market, as persistent capital needs weren't around. This changed with the building of the so-called New World, namely the Americas, starting about 500 years ago.

## The Birth of Joint Stock Companies

With the discovery of the so-called New World, came new projects on a scale never seen before. With a newfound need for capital, came the invention of companies. At first it was the sovereign who would finance a particular project. When the sovereigns were stretched financially, they turned to the nobility and occasionally to the merchant classes to pay for a new project.

This process needed a structure to organize the money, resulting in "joint stock companies," in which the ownership was dispersed among several noble families or those who had wealth.

## From Illiquidity to Speculation

There was no financial speculation at this point in time, because investments were illiquid. Participating in a venture in the New World meant risk and lack of knowledge. Once the investment was made—whether a goldmine or a new canal—the way to make money on that investment was by reaping the rewards of profit, by way of dividends.

Introducing the ability to dispose of investments opened a door to liquidity, and short-term decision making. And with that, human psychology entered the business of investment. Lowering the barrier to sell made way for human instincts, such as greed, the desire to get rich quickly—to influence the price.

## The Modern Reality

The end result was a growing market that mobilized capital from all elements of society. Speculation made the stock market a powerful aggregator of capital, beyond anything seen before.

Today, about 80% of the volume is S&P 500. Meanwhile 60 plus percent of the market is passive. The reference benchmark is S&P 500. Thirty-nine percent or so of the market is 10 stocks. Four stocks have determined basically the profitability of the market for the last four years.

The structure of our market has changed. It doesn't mean bad, it just means indexed and correlated and different.`,
  },
  {
    id: "compounding-capital",
    title: "Compounding Capital: Exponential Growth",
    category: "philosophy",
    description: "Why exponential growth is counterintuitive and how to harness it for long-term wealth creation.",
    readTime: "6 min",
    source: "The Ocean Doesn't Care About Your Swimming Lessons",
    content: `# Compounding Capital Means Exponential Growth, and It's Not Intuitive

Growing our and our partners' money exponentially is contrary to human psychology. It is contrary to the incentives and structure of financial institutions. It is contrary to the investing norms.

The culture of using time to grow capital is so simple that it is a topic of never-ending discussions but seldom observed in terms of the investors' behavior.

## The Power of Time

Throughout the office, we have one table framed on most walls. The table has numbers, but it is a qualitative message about our mission over the next 32 years. I am 41 years old and savor the journey to this first goal post. It may be peculiar to think in these time horizons. That said, it's the appropriate lens.

Time is our steadfast and dependable ally that supports our goals. Conversely, time tends to work against our investing competition. We savor the passage of time, because it thins out competitors, compounds our capital, and gives us a compounding of knowledge in specific areas. Thereby deepening our edge in those areas.

## Why Most Fail to Compound

If there was one thing to understand and repeat about Marlowe it would be time. A long time.

The layers of complexity around the casino of the market are perhaps more pernicious, in that outcomes that are in truth random, are misunderstood as being the result of skill. The introduction of equity investing pandered to such base instincts.

Behind gambling behavior is a soup of cognitive biases that could be short-hand for believing in luck. These biases are monetized in the Gaming Industry, but perhaps the largest source of profit from similar biases stems from the trading commissions, taxes, and other frictional costs that come from frequent buying and selling.

## The Marlowe Approach

My preference is for results to be measured over a five-year time frame. However, whenever there is an opportunity to learn, we will learn. So we may be disappointed with a stock price in the short term, but you can't learn from that. However, suppose management makes an unforced error in a capital allocation decision, or we realize we have fundamentally misunderstood an aspect of the business model. In that case, this is worth much more than a stock price going up or down.`,
  },
  {
    id: "investing-vs-speculating",
    title: "Investing vs. Speculating",
    category: "philosophy",
    description: "The critical distinction between owning businesses and trading stocks.",
    readTime: "5 min",
    source: "The Ocean Doesn't Care About Your Swimming Lessons",
    content: `# Investing vs. Speculating

Do you feel you own a business if you own its stock? I do.

This simple question reveals the fundamental divide in how people approach the stock market. The speculator sees stocks as tickets to be traded. The investor sees stocks as ownership stakes in real businesses.

## Self-Awareness is Critical

Many ways to make money exist in markets, but be mindful of what you're actually doing. The speculator and the investor can both make money, but they are playing entirely different games with different rules and different odds.

Marlowe is an Investor. We are authentically investing for the long term in modestly valued companies run by management teams that, at times, will make decisions whose fruits may not be reflected in the stock price for years.

## The Ownership Mentality

When I look around, I think, well, where are the Walmart billionaires aside from the families? Where are the non-family Walmart billionaires?

As you dig, you will soon find that Walmart is one of many such publicly traded companies from an early phase. Given this, the public, whether a sophisticated hedge fund or a personal investor investing their retirement savings, has had and continues to have access to some of the most exceptional businesses ever created. And yet, where are the people who benefited? Where are the people who will take those shares and pass them on to the next generation?

## Why This Matters

This is why my fund exists. To capture the wealth creation that the investment industry systematically fails to capture, despite having access to the same great businesses as founders and early investors.

The near-term results are likely to be as bad as they may be very good. But we are confident, not so much in the prediction of good or bad, but that in the long run they will prove satisfactory.`,
  },
  // Risk Management
  {
    id: "how-marlowe-sees-risk",
    title: "How Marlowe Sees Risk",
    category: "risk",
    description: "A different perspective on risk that focuses on permanent loss of capital, not volatility.",
    readTime: "7 min",
    source: "The Ocean Doesn't Care About Your Swimming Lessons",
    content: `# How Marlowe Sees Risk

Risk is not volatility. Risk is the permanent loss of capital.

This distinction is fundamental to how we operate. The financial industry has convinced most investors that volatility equals risk. This conflation serves the industry well—it justifies constant activity, frequent rebalancing, and the sale of "risk management" products.

## Marlowe's View on Risk is Common Sense

A good risk has a narrow asymmetrical outcome. When we take risk, we want the potential upside to far exceed the potential downside. This is not about avoiding risk—it's about taking intelligent risk.

Concentration in high-conviction ideas reduces risk when you truly understand the business. The stapler analogy: if you had to bet your life on a stapler working, would you rather have one high-quality stapler you've tested thoroughly, or ten random staplers you've never used?

## The Manager's Motivation, Tenure, and Commitment

We look at the manager's motivation, tenure, and commitment to the business. Are they invested alongside us? Have they been through difficult periods? Do they have skin in the game?

## Useful Worrying

We engage in what we call "useful worrying." This means constantly asking: what could go wrong? What are we missing? What would make us sell?

## Learning from Risk

Every investment teaches us something about risk. We document our mistakes and update our process. The goal is not to avoid all mistakes—that's impossible—but to learn from each one and avoid repeating it.`,
  },
  {
    id: "concentration-vs-diversification",
    title: "Concentration vs. Diversification",
    category: "risk",
    description: "Why concentrated portfolios can be less risky than diversified ones when you truly understand your investments.",
    readTime: "6 min",
    source: "The Ocean Doesn't Care About Your Swimming Lessons",
    content: `# Concentration vs. Diversification

Diversification is protection against ignorance. It makes little sense if you know what you are doing.

This is a controversial statement in modern finance, but we believe it deeply. The conventional wisdom that diversification always reduces risk is simply wrong when applied to investors who truly understand their holdings.

## The Case for Concentration

If you have a genuine edge—deep knowledge of a business, its industry, and its management—why would you dilute that edge by spreading your capital across dozens of positions you understand less well?

Our best ideas deserve our most capital. Our tenth-best idea should not receive the same allocation as our best idea.

## When Diversification Makes Sense

Diversification makes sense when you're uncertain. If you're indexing or don't have the time or skill to analyze individual businesses, broad diversification is prudent.

But for professional investors who dedicate their lives to understanding businesses, excessive diversification is a crutch that masks a lack of conviction.

## The Marlowe Approach

We typically hold 10-15 positions. Each position represents a business we understand deeply. We know the management, the competitive dynamics, the financial structure, and the key risks.

This concentration allows us to monitor our investments closely and react quickly when circumstances change.`,
  },
  // Market Psychology
  {
    id: "market-psychology",
    title: "Understanding Market Psychology",
    category: "psychology",
    description: "How human emotions drive market behavior and create opportunities for rational investors.",
    readTime: "8 min",
    source: "The Ocean Doesn't Care About Your Swimming Lessons",
    content: `# Understanding Market Psychology

The market is not a weighing machine in the short term. It's a voting machine, driven by emotion, narrative, and momentum.

Understanding this distinction is crucial for long-term investors. In the short term, prices are determined by psychology. In the long term, they're determined by fundamentals.

## Fear and Greed

The two dominant emotions in markets are fear and greed. They alternate in cycles, creating booms and busts. The wise investor learns to recognize these cycles and act contrary to the crowd.

When others are greedy, be fearful. When others are fearful, be greedy. This simple advice is extraordinarily difficult to follow in practice.

## The Narrative Machine

Markets love narratives. A compelling story can drive prices far beyond fundamental value. The dot-com bubble was driven by the narrative of the "new economy." The housing bubble was driven by the narrative that "housing prices never fall."

We try to separate narrative from reality. What are the actual cash flows? What is the business actually worth?

## Patience as an Edge

Most market participants have short time horizons. Quarterly earnings, annual performance reviews, and career risk all push investors toward short-term thinking.

Our edge is patience. We can wait for the market to recognize value that others are too impatient to see.`,
  },
  {
    id: "behavioral-biases",
    title: "Behavioral Biases in Investing",
    category: "psychology",
    description: "The cognitive traps that cause investors to make systematic errors.",
    readTime: "7 min",
    source: "The Ocean Doesn't Care About Your Swimming Lessons",
    content: `# Behavioral Biases in Investing

We are not rational beings. We are rationalizing beings. Our brains are wired with shortcuts that served us well on the savanna but lead us astray in financial markets.

## Confirmation Bias

We seek information that confirms our existing beliefs and ignore information that contradicts them. This is perhaps the most dangerous bias for investors.

The antidote is to actively seek disconfirming evidence. When we make an investment, we assign someone to argue the bear case.

## Loss Aversion

We feel losses about twice as strongly as equivalent gains. This leads us to hold losers too long (hoping to break even) and sell winners too soon (locking in gains).

The solution is to focus on expected value, not on whether a position is currently profitable.

## Recency Bias

We overweight recent events and underweight historical base rates. After a market crash, we expect more crashes. After a boom, we expect more boom.

The solution is to study history and remember that cycles repeat.

## Overconfidence

We systematically overestimate our abilities and the precision of our forecasts. This leads to excessive trading and inadequate diversification.

The solution is humility. Acknowledge uncertainty. Use ranges, not point estimates.`,
  },
  // Market History
  {
    id: "lessons-from-history",
    title: "Lessons from Market History",
    category: "history",
    description: "What past market cycles teach us about human nature and investing.",
    readTime: "9 min",
    source: "The Ocean Doesn't Care About Your Swimming Lessons",
    content: `# Lessons from Market History

Those who cannot remember the past are condemned to repeat it. This is especially true in financial markets, where the same patterns recur generation after generation.

## The Persistence of Bubbles

Bubbles are not anomalies. They are a regular feature of financial markets. From the Dutch tulip mania to the dot-com bubble to the housing crisis, the pattern is remarkably consistent.

Prices rise far above fundamental value, driven by speculation and easy credit. Eventually, reality intrudes, and prices collapse. The cycle then repeats.

## The Importance of Survival

The first rule of compounding is to never interrupt it unnecessarily. Many investors who were "right" about bubbles still lost money because they were early or used leverage.

Survival is more important than maximizing returns. A 50% loss requires a 100% gain to recover.

## Mean Reversion

Extreme valuations tend to revert to historical averages. High P/E ratios predict low future returns. Low P/E ratios predict high future returns.

This doesn't help with timing, but it helps with expectations. When valuations are extreme, adjust your return expectations accordingly.

## The Power of Compounding

The most important lesson from history is the power of compounding. Patient investors who stayed the course through multiple cycles accumulated enormous wealth.

The key is to survive the downturns and remain invested for the long term.`,
  },
  // Investment Process
  {
    id: "research-process",
    title: "The Marlowe Keynes Process",
    category: "process",
    description: "How we analyze businesses and make investment decisions.",
    readTime: "10 min",
    source: "The Ocean Doesn't Care About Your Swimming Lessons",
    content: `# The Marlowe Keynes Process

Our research process is designed to understand businesses deeply before investing. We are not looking for quick trades or momentum plays. We are looking for businesses we can own for years.

## Step 1: Idea Generation

Ideas come from many sources: industry contacts, reading, screens, and serendipity. We cast a wide net but are highly selective about what we pursue.

## Step 2: Initial Assessment

Before diving deep, we ask: Is this business within our circle of competence? Is it the type of business that can compound value over time? Is the valuation potentially attractive?

If the answer to any of these is no, we move on.

## Step 3: Deep Research

For ideas that pass initial screening, we conduct extensive research. We read every public filing. We talk to customers, competitors, and industry experts. We build detailed financial models.

This process typically takes months, not days.

## Step 4: Investment Decision

After research is complete, we make a decision. We require high conviction to invest. If we're uncertain, we pass.

## Step 5: Position Sizing

Position size reflects conviction and risk. Our highest-conviction ideas receive the most capital. We never bet the farm on any single idea.

## Step 6: Ongoing Monitoring

Research doesn't stop after we invest. We continuously monitor our holdings and update our views as new information emerges.`,
  },
  {
    id: "valuation-framework",
    title: "Our Valuation Framework",
    category: "process",
    description: "How we think about what a business is worth.",
    readTime: "8 min",
    source: "The Ocean Doesn't Care About Your Swimming Lessons",
    content: `# Our Valuation Framework

Valuation is both art and science. The science is the math—discounted cash flows, comparable analysis, and sum-of-the-parts. The art is the judgment—what assumptions to use, what risks to weight, what the future might hold.

## Intrinsic Value

We define intrinsic value as the present value of all future cash flows. This is conceptually simple but practically difficult. It requires forecasting cash flows far into the future and choosing an appropriate discount rate.

## Margin of Safety

We never pay intrinsic value. We require a margin of safety—a discount to our estimate of intrinsic value. This margin protects us from errors in our analysis and unforeseen events.

## Multiple Scenarios

We don't rely on a single forecast. We model multiple scenarios: base case, bull case, and bear case. We weight these scenarios by probability and look at the expected value.

## Qualitative Factors

Numbers don't tell the whole story. We also consider qualitative factors: management quality, competitive position, industry dynamics, and regulatory environment.

## Valuation is Not Timing

Knowing that something is undervalued doesn't tell you when it will be recognized. We may be early. We may be wrong. But over time, value tends to be recognized.`,
  },
  // Marlowe Culture
  {
    id: "marlowe-culture",
    title: "The Marlowe Culture",
    category: "culture",
    description: "The values and principles that guide how we work.",
    readTime: "7 min",
    source: "The Ocean Doesn't Care About Your Swimming Lessons",
    content: `# The Marlowe Culture

Culture is not what you say. It's what you do. At Marlowe, we try to live our values every day.

## Intellectual Honesty

We prize intellectual honesty above all else. We want to know the truth, even when it's uncomfortable. We encourage dissent and debate. We change our minds when the evidence warrants.

## Long-Term Thinking

We think in years and decades, not quarters. This affects everything from how we invest to how we compensate our team.

## Continuous Learning

We are students of business and markets. We read voraciously. We seek out experts. We never stop learning.

## Humility

We know we will be wrong. We don't know when or how, but we know it will happen. This humility keeps us cautious and open-minded.

## Partnership

We treat our investors as partners, not customers. Their interests are our interests. We eat our own cooking.

## Simplicity

We favor simple over complex. Simple strategies are easier to understand, easier to execute, and easier to stick with during difficult times.`,
  },
  {
    id: "finding-joy",
    title: "Finding Joy in the Challenge",
    category: "culture",
    description: "Why we love what we do and how that passion drives results.",
    readTime: "6 min",
    source: "The Ocean Doesn't Care About Your Swimming Lessons",
    content: `# Finding Joy in the Challenge

The most rewarding investments are often the most difficult. If it were easy, everyone would do it, and there would be no excess returns.

## Don't Confuse Obedience with Intelligence

Following the crowd is easy. Thinking independently is hard. We value independent thinking over credentials or conformity.

## Mandate Requires Obsessive Curiosity

Our mandate requires obsessive curiosity and relentlessness. We are not satisfied with surface-level understanding. We dig until we truly understand.

## Work Culture

Hard work never killed anyone. We embrace the difficulty because we know it's the source of our edge. The mediocre are always at their best—we strive to continuously improve.

## Managing Fear as a Business Owner

Fear is natural. The question is how we channel it. Useful fear drives preparation and caution. Destructive fear drives paralysis and panic selling.

## Day-to-Day Routine

Consistency matters. We show up every day, do the work, and trust the process. The compound effect of daily effort is enormous over time.

## Unified Vision and the Power of Belief

Everyone at Marlowe shares the same vision. This alignment creates power that fragmented organizations cannot match.

## Patience

Patience is perhaps our greatest competitive advantage. In a world of instant gratification, the ability to wait is rare and valuable.`,
  },
  {
    id: "capital-allocation",
    title: "Capital Allocation Decisions",
    category: "culture",
    description: "How management teams should think about deploying capital.",
    readTime: "6 min",
    source: "The Ocean Doesn't Care About Your Swimming Lessons",
    content: `# Capital Allocation Decisions

Capital allocation is the most important job of a CEO. How a company deploys its capital determines its long-term value creation.

## The CEO's Role

The CEO must be a capital allocator first and an operator second. Many excellent operators are poor capital allocators, destroying value through ill-conceived acquisitions or poorly timed buybacks.

## The CFO's Role

The CFO must be the guardian of capital discipline. They must push back against empire-building and ensure every dollar is deployed for maximum return.

## Compensation Schemes and Incentives

Incentives drive behavior. We look for management teams whose compensation is aligned with long-term shareholder value, not short-term stock price or accounting metrics.

## Buybacks and Dividends

Buybacks are good when stock is undervalued and bad when stock is overvalued. Most companies get this backwards, buying back stock when times are good (and prices high) and stopping when times are bad (and prices low).

Dividends signal discipline. A company that commits to a dividend must maintain capital discipline to fund it.

## Capital Allocation and Human Progress

Good capital allocation drives human progress. Capital flows to its highest and best use, funding innovation and growth. Poor capital allocation wastes resources and slows progress.

## Business Incentives

We study incentives carefully. What gets measured gets managed. What gets rewarded gets done. Understanding incentives helps us predict behavior.`,
  },
];

export default function Library() {
  const { user, isAuthenticated } = useAuth();
  const [selectedCategory, setSelectedCategory] = useState("all");
  const [searchQuery, setSearchQuery] = useState("");
  const [selectedEssay, setSelectedEssay] = useState<Essay | null>(null);
  const [allEssays, setAllEssays] = useState<Essay[]>(essays);
  const readingStartTime = useRef<number | null>(null);

  // Load new essays from JSON file
  useEffect(() => {
    fetch('/essays/new-essays.json')
      .then(res => res.json())
      .then((data: Essay[]) => {
        setAllEssays([...essays, ...data]);
      })
      .catch(err => console.error('Failed to load new essays:', err));
  }, []);
  
  // Reading progress queries
  const { data: progressData, refetch: refetchProgress } = trpc.essayProgress.getAll.useQuery(
    undefined, { ...QUERY_CACHE.STATIC,  enabled: isAuthenticated });
  const { data: statsData } = trpc.essayProgress.getStats.useQuery(
    undefined, { ...QUERY_CACHE.STATIC,  enabled: isAuthenticated });
  
  // Reading progress mutations
  const markReadMutation = trpc.essayProgress.markRead.useMutation({
    onSuccess: () => {
      refetchProgress();
      toast.success("Essay marked as read!");
    },
  });
  
  const updateReadTimeMutation = trpc.essayProgress.updateReadTime.useMutation();
  
  // Get set of read essay IDs
  const readEssayIds = new Set(
    progressData?.filter(p => p.isCompleted).map(p => p.essayId) || []
  );

  // Build scroll progress map for essay cards
  const essayProgressMap = useMemo(() => {
    const map = new Map<string, { scrollProgress: number; isCompleted: boolean }>();
    if (progressData) {
      for (const p of progressData) {
        map.set(p.essayId, {
          scrollProgress: (p as any).scrollProgress || 0,
          isCompleted: p.isCompleted || false,
        });
      }
    }
    return map;
  }, [progressData]);
  
  // Calculate reading progress percentage
  const totalEssays = allEssays.length;
  const readCount = statsData?.totalRead || 0;
  const progressPercent = totalEssays > 0 ? Math.round((readCount / totalEssays) * 100) : 0;

  // Scroll tracking for essay reading view
  const scrollRef = useRef<HTMLDivElement>(null);
  const [scrollPercent, setScrollPercent] = useState(0);
  const lastSavedScrollProgress = useRef(0);
  const updateScrollProgressMutation = trpc.essayProgress.updateScrollProgress.useMutation({
    onSuccess: () => refetchProgress(),
  });
  
  // TTS state
  const [audioData, setAudioData] = useState<string | null>(null);
  const [audioUrl, setAudioUrl] = useState<string | null>(null);
  const [audioFormat, setAudioFormat] = useState<string>('mp3');
  const [isGeneratingAudio, setIsGeneratingAudio] = useState(false);
  const [selectedVoiceId, setSelectedVoiceId] = useState<string>(() => {
    if (typeof window !== 'undefined') {
      return localStorage.getItem('speechify-preferred-voice') || 'george';
    }
    return 'george';
  });
  
  // Fetch available voices
  const { data: voices = [] } = trpc.speechify.getVoices.useQuery(undefined, QUERY_CACHE.SLOW_MOVING);
  
  // TTS mutation
  const generateSpeech = trpc.speechify.generateLongSpeech.useMutation({
    onSuccess: (data) => {
      // Prefer URL (S3 cached) over base64 data
      setAudioUrl(data.audioUrl || null);
      setAudioData(data.audioData || null);
      setAudioFormat(data.audioFormat);
      setIsGeneratingAudio(false);
    },
    onError: (error) => {
      const translated = errorTranslators.audio(error.message);
      toast.error(translated.message, {
        description: translated.description,
      });
      setIsGeneratingAudio(false);
    },
  });
  
  // Bookmarks state
  const [showBookmarks, setShowBookmarks] = useState(false);
  const contentRef = useRef<HTMLElement>(null);
  const bookmarkUtils = trpc.useUtils();
  const { data: bookmarks = [], isLoading: bookmarksLoading } = trpc.essayBookmarks.getAll.useQuery(
    { essayId: selectedEssay?.id || '' }, { ...QUERY_CACHE.STATIC,  enabled: isAuthenticated && !!selectedEssay });
  const addBookmarkMutation = trpc.essayBookmarks.add.useMutation({
    onSuccess: () => {
      bookmarkUtils.essayBookmarks.getAll.invalidate({ essayId: selectedEssay?.id || '' });
      toast.success('Bookmark saved!');
    },
    onError: () => toast.error('Failed to save bookmark'),
  });
  const removeBookmarkMutation = trpc.essayBookmarks.remove.useMutation({
    onSuccess: () => {
      bookmarkUtils.essayBookmarks.getAll.invalidate({ essayId: selectedEssay?.id || '' });
      toast.success('Bookmark removed');
    },
    onError: () => toast.error('Failed to remove bookmark'),
  });
  const updateBookmarkMutation = trpc.essayBookmarks.update.useMutation({
    onSuccess: () => {
      bookmarkUtils.essayBookmarks.getAll.invalidate({ essayId: selectedEssay?.id || '' });
      toast.success('Bookmark updated');
    },
    onError: () => toast.error('Failed to update bookmark'),
  });

  // Handle listen button click
  const handleListen = () => {
    if (!selectedEssay) return;
    if (audioData || audioUrl) {
      // Already have audio, just toggle player visibility
      return;
    }
    setIsGeneratingAudio(true);
    // Strip markdown formatting for cleaner audio
    const cleanText = selectedEssay.content
      .replace(/^#+\s*/gm, '') // Remove headers
      .replace(/\*\*/g, '') // Remove bold
      .replace(/\*/g, '') // Remove italic
      .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove links, keep text
      .trim();
    generateSpeech.mutate({ text: cleanText, voiceId: selectedVoiceId });
  };
  
  // Handle voice change
  const handleVoiceChange = (voiceId: string) => {
    setSelectedVoiceId(voiceId);
    if (typeof window !== 'undefined') {
      localStorage.setItem('speechify-preferred-voice', voiceId);
    }
    // Clear cached audio when voice changes
    setAudioData(null);
    setAudioUrl(null);
  };
  
  // Reset audio and track reading time when essay changes
  const handleSelectEssay = (essay: Essay | null) => {
    // Save reading time for previous essay
    if (selectedEssay && readingStartTime.current && isAuthenticated) {
      const readingTime = Math.floor((Date.now() - readingStartTime.current) / 1000);
      if (readingTime > 5) { // Only track if read for more than 5 seconds
        updateReadTimeMutation.mutate({
          essayId: selectedEssay.id,
          additionalSeconds: readingTime,
        });
      }
    }
    
    // Set new essay and start timer
    setSelectedEssay(essay);
    readingStartTime.current = essay ? Date.now() : null;
    
    // Reset audio state
    setAudioData(null);
    setAudioUrl(null);
    setAudioFormat('mp3');
    setIsGeneratingAudio(false);
  };
  
  // Mark essay as read
  const handleMarkAsRead = () => {
    if (!selectedEssay || !isAuthenticated) return;
    
    const readingTime = readingStartTime.current 
      ? Math.floor((Date.now() - readingStartTime.current) / 1000)
      : 0;
    
    markReadMutation.mutate({
      essayId: selectedEssay.id,
      readTimeSeconds: readingTime,
    });
  };

  // Filter essays
  const filteredEssays = allEssays.filter(essay => {
    const matchesCategory = selectedCategory === "all" || essay.category === selectedCategory;
    const matchesSearch = essay.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
                         essay.description.toLowerCase().includes(searchQuery.toLowerCase());
    return matchesCategory && matchesSearch;
  });

  // Download essay as text
  const handleDownload = (essay: Essay) => {
    const content = `${essay.title}\n\nBy David Steinberg\nFrom: ${essay.source || "Marlowe Keynes"}\n\n${essay.content.replace(/^#+ /gm, '').replace(/\*\*/g, '')}`;
    const blob = new Blob([content], { type: "text/plain" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `${essay.id}.txt`;
    a.click();
    URL.revokeObjectURL(url);
  };

  // Dynamic SEO: update OG tags when viewing a specific essay
  SEOHead(
    selectedEssay
      ? {
          title: `${selectedEssay.title} | Marlowe Keynes Learn`,
          description: selectedEssay.content
            ? selectedEssay.content.replace(/[#*_\[\]()>]/g, '').slice(0, 155).trim() + '...'
            : pageSEO.library.description,
          image: pageSEO.library.image,
          type: 'article' as const,
        }
      : pageSEO.library
  );

  const currentBreadcrumbs = selectedEssay
    ? breadcrumbs.libraryEssay(selectedEssay.title, selectedEssay.id)
    : breadcrumbs.library;

  return (
    <PageTransition>
    <div className="min-h-screen bg-background text-foreground">
      <BreadcrumbJsonLd items={currentBreadcrumbs} />
      {/* Header */}
      <header className="sticky top-0 z-50 glass border-b border-border/30">
        <div className="container mx-auto px-4 py-3">
          <div className="flex items-center justify-between">
            <div className="flex items-center gap-4">
              <Link href="/">
                <Button variant="ghost" size="icon" aria-label="Action" className="rounded-full">
                  <ArrowLeft className="h-5 w-5" />
                </Button>
              </Link>
              <div className="flex items-center gap-3">
                <div className="w-10 h-10 rounded-full bg-gradient-to-br from-primary/20 to-primary/40 flex items-center justify-center">
                  <BookOpen className="h-5 w-5 text-foreground/60" />
                </div>
                <div>
                  <h1 className="text-lg font-semibold tracking-tight font-editorial">Library</h1>
                  <p className="text-xs text-muted-foreground">Original Essays by David Steinberg</p>
                </div>
              </div>
            </div>
            
            {/* Reading Progress Stats */}
            {isAuthenticated && statsData && (
              <div className="hidden md:flex items-center gap-4 bg-secondary/50 rounded-lg px-4 py-2">
                <div className="flex items-center gap-2">
                  <Trophy className="h-4 w-4 text-primary" />
                  <span className="text-sm font-semibold">{readCount} / {totalEssays}</span>
                  <span className="text-xs text-muted-foreground">read</span>
                </div>
                <div className="w-40">
                  <Progress value={progressPercent} className="h-2.5" />
                </div>
                <span className="text-sm font-mono font-medium text-primary">{progressPercent}%</span>
              </div>
            )}
          </div>
        </div>
      </header>

      <div className="container mx-auto px-4 py-6">
        {/* Breadcrumb Navigation */}
        <div className="mb-4">
          <Breadcrumb items={[
            { label: "Library" },
          ]} />
        </div>
        {/* Reading View */}
        <AnimatePresence>
          {selectedEssay && (
            <motion.div
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              className="fixed inset-0 z-50 bg-background"
            >
              {/* Reading progress bar at the very top */}
              <div className="fixed top-0 left-0 right-0 z-[60] h-1 bg-muted">
                <motion.div
                  className="h-full bg-primary"
                  initial={{ width: 0 }}
                  animate={{ width: `${scrollPercent}%` }}
                  transition={{ duration: 0.15, ease: 'linear' }}
                />
              </div>

              {/* Reading Header */}
              <div className="sticky top-0 glass-strong backdrop-blur-md border-b border-border pt-1">
                <div className="container mx-auto px-4 py-3">
                  <div className="flex items-center justify-between">
                    <Button
                      variant="ghost"
                      size="sm"
                      onClick={() => handleSelectEssay(null)}
                      className="gap-2"
                    >
                      <ArrowLeft className="h-4 w-4" />
                      Back to Library
                    </Button>
                    <div className="flex items-center gap-2">
                      {/* Scroll progress indicator */}
                      <span className="text-xs text-muted-foreground font-mono hidden sm:block">
                        {scrollPercent}%
                      </span>
                      {/* Mark as Read Button */}
                      {isAuthenticated && (
                        <Button
                          variant={readEssayIds.has(selectedEssay.id) ? "secondary" : "outline"}
                          size="sm"
                          onClick={handleMarkAsRead}
                          disabled={markReadMutation.isPending || readEssayIds.has(selectedEssay.id)}
                          className="gap-2"
                        >
                          {readEssayIds.has(selectedEssay.id) ? (
                            <>
                              <CheckCircle2 className="h-4 w-4 text-emerald-700 dark:text-emerald-400" />
                              Read
                            </>
                          ) : markReadMutation.isPending ? (
                            <>
                              <Loader2 className="h-4 w-4 animate-spin" />
                              Marking...
                            </>
                          ) : (
                            <>
                              <Circle className="h-4 w-4" />
                              Mark as Read
                            </>
                          )}
                        </Button>
                      )}
                      {/* Bookmarks Button */}
                      {isAuthenticated && (
                        <Button
                          variant="outline"
                          size="sm"
                          onClick={() => setShowBookmarks(true)}
                          className="gap-1.5 relative"
                        >
                          <Bookmark className="h-4 w-4" />
                          <span className="hidden sm:inline">Bookmarks</span>
                          {bookmarks.length > 0 && (
                            <span className="absolute -top-1.5 -right-1.5 bg-primary text-primary-foreground text-[10px] font-bold rounded-full w-4 h-4 flex items-center justify-center">
                              {bookmarks.length}
                            </span>
                          )}
                        </Button>
                      )}
                      {/* Voice Selector */}
                      {voices.length > 0 && (
                        <VoiceSelector
                          voices={voices}
                          selectedVoiceId={selectedVoiceId}
                          onVoiceChange={handleVoiceChange}
                          disabled={isGeneratingAudio}
                        />
                      )}
                      <Button
                        variant="outline"
                        size="sm"
                        onClick={handleListen}
                        disabled={isGeneratingAudio}
                        className="gap-2"
                      >
                        {isGeneratingAudio ? (
                          <>
                            <Loader2 className="h-4 w-4 animate-spin" />
                            Generating...
                          </>
                        ) : (
                          <>
                            <Volume2 className="h-4 w-4" />
                            Listen
                          </>
                        )}
                      </Button>
                      {isAuthenticated && selectedEssay && (
                        <ElevenLabsListenButton
                          text={selectedEssay.content
                            .replace(/^#+\s*/gm, '')
                            .replace(/\*\*/g, '')
                            .replace(/\*/g, '')
                            .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
                            .trim()}
                          label="ElevenLabs"
                          variant="outline"
                          size="sm"
                          longText
                          showPlayer
                          tooltip="Listen with ElevenLabs HD voice"
                        />
                      )}
                      <Button
                        variant="outline"
                        size="sm"
                        onClick={() => handleDownload(selectedEssay)}
                        className="gap-2"
                      >
                        <Download className="h-4 w-4" />
                        Download
                      </Button>
                    </div>
                  </div>
                </div>
              </div>

              {/* Reading Content */}
              <LibraryScrollTracker
                scrollRef={scrollRef}
                essayId={selectedEssay.id}
                isAuthenticated={isAuthenticated}
                onScrollChange={setScrollPercent}
                lastSavedRef={lastSavedScrollProgress}
                updateMutation={updateScrollProgressMutation}
              />
              <ScrollArea className="h-[calc(100vh-60px)]" ref={scrollRef}>
                <article className="container mx-auto px-4 py-8 max-w-3xl relative" ref={contentRef as any}>
                  <header className="mb-8">
                    <div className="flex items-center gap-2 mb-2">
                      <p className="text-sm text-foreground font-mono uppercase tracking-wider">
                        {categories.find(c => c.id === selectedEssay.category)?.name}
                      </p>
                      {readEssayIds.has(selectedEssay.id) && (
                        <span className="flex items-center gap-1 text-xs text-emerald-700 dark:text-emerald-400 bg-green-500/10 px-2 py-0.5 rounded-full">
                          <CheckCircle2 className="h-3 w-3" />
                          Read
                        </span>
                      )}
                    </div>
                    <h1 className="text-3xl md:text-4xl font-display font-semibold mb-4">
                      {selectedEssay.title}
                    </h1>
                    <div className="flex items-center gap-4 text-sm text-muted-foreground">
                      <span className="flex items-center gap-1">
                        <Clock className="h-4 w-4" />
                        {formatReadingTime(selectedEssay.content)}
                      </span>
                      <span className="flex items-center gap-1 text-xs text-muted-foreground/70">
                        {getReadingStats(selectedEssay.content).wordCount.toLocaleString()} words
                      </span>
                      {selectedEssay.source && (
                        <span className="flex items-center gap-1">
                          <BookOpen className="h-4 w-4" />
                          {selectedEssay.source}
                        </span>
                      )}
                    </div>
                    
                    {/* Audio Player - appears when audio is generated */}
                    {(audioData || audioUrl) && (
                      <div className="mt-4">
                        <AudioPlayer
                          audioData={audioUrl ? undefined : audioData || undefined}
                          src={audioUrl || undefined}
                          audioFormat={audioFormat}
                          showSpeedControl
                          showKeyboardHints
                          autoPlay
                        />
                      </div>
                    )}
                  </header>

                  <div className="prose prose-lg dark:prose-invert max-w-none">
                    <Streamdown>{selectedEssay.content}</Streamdown>
                  </div>

                  <footer className="mt-12 pt-8 border-t border-border">
                    <div className="flex items-center justify-between">
                      <p className="text-sm text-muted-foreground italic">
                        From the writings of David Steinberg, Founder & Chief Investment Officer of Marlowe Partners.
                      </p>
                      {isAuthenticated && !readEssayIds.has(selectedEssay.id) && (
                        <Button
                          variant="default"
                          size="sm"
                          onClick={handleMarkAsRead}
                          disabled={markReadMutation.isPending}
                          className="gap-2"
                        >
                          {markReadMutation.isPending ? (
                            <Loader2 className="h-4 w-4 animate-spin" />
                          ) : (
                            <CheckCircle2 className="h-4 w-4" />
                          )}
                          Mark as Read
                        </Button>
                      )}
                    </div>
                  </footer>
                </article>
              </ScrollArea>

              {/* Bookmark text selection popup */}
              {isAuthenticated && contentRef.current && (
                <BookmarkPopup
                  isAuthenticated={isAuthenticated}
                  containerRef={contentRef}
                  isSaving={addBookmarkMutation.isPending}
                  onSave={(data) => {
                    addBookmarkMutation.mutate({
                      essayId: selectedEssay.id,
                      ...data,
                    });
                  }}
                />
              )}

              {/* Bookmarks panel */}
              <BookmarksPanel
                isOpen={showBookmarks}
                onClose={() => setShowBookmarks(false)}
                bookmarks={bookmarks}
                isLoading={bookmarksLoading}
                essayTitle={selectedEssay.title}
                onDelete={(bookmarkId) => removeBookmarkMutation.mutate({ bookmarkId })}
                onUpdate={(bookmarkId, data) => updateBookmarkMutation.mutate({ bookmarkId, ...data })}
                isDeleting={removeBookmarkMutation.isPending}
                isUpdating={updateBookmarkMutation.isPending}
              />
            </motion.div>
          )}
        </AnimatePresence>

        {/* Main Content */}
        {!selectedEssay && (
          <>
            {/* Reading Progress Card (Mobile) */}
            {isAuthenticated && statsData && (
              <div className="md:hidden mb-6 p-4 bg-card border border-border rounded-xl">
                <div className="flex items-center justify-between mb-2">
                  <div className="flex items-center gap-2">
                    <Trophy className="h-5 w-5 text-foreground/60" />
                    <span className="font-medium">Reading Progress</span>
                  </div>
                  <span className="text-sm text-muted-foreground">{progressPercent}%</span>
                </div>
                <Progress value={progressPercent} className="h-2 mb-2" />
                <p className="text-xs text-muted-foreground">
                  {readCount} of {totalEssays} essays completed
                </p>
              </div>
            )}
          
            {/* Continue Reading Section */}
            {isAuthenticated && (() => {
              const inProgressEssays = allEssays.filter(essay => {
                const progress = essayProgressMap.get(essay.id);
                return progress && progress.scrollProgress > 0 && !progress.isCompleted;
              }).sort((a, b) => {
                const pA = essayProgressMap.get(a.id)?.scrollProgress || 0;
                const pB = essayProgressMap.get(b.id)?.scrollProgress || 0;
                return pB - pA;
              }).slice(0, 4);

              if (inProgressEssays.length === 0) return null;

              return (
                <div className="mb-8">
                  <div className="flex items-center gap-2 mb-4">
                    <BookOpen className="h-5 w-5 text-foreground/60" />
                    <h3 className="text-lg font-semibold">Continue Reading</h3>
                    <span className="text-xs text-muted-foreground font-mono ml-auto">
                      {inProgressEssays.length} in progress
                    </span>
                  </div>
                  <div className="grid gap-3">
                    {inProgressEssays?.length === 0 && <EmptyState variant="generic" title="No data yet" description="Content will appear here once available." />}
                {inProgressEssays.map((essay) => {
                      const scrollPct = essayProgressMap.get(essay.id)?.scrollProgress || 0;
                      return (
                        <motion.div
                          key={essay.id}
                          initial={{ opacity: 0, x: -10 }}
                          animate={{ opacity: 1, x: 0 }}
                          className="group glass-card border-primary/20 rounded-xl p-4 hover:border-primary/50 transition-[colors,opacity,transform,shadow] cursor-pointer relative overflow-hidden"
                          onClick={() => handleSelectEssay(essay)}
                        >
                          <div className="absolute bottom-0 left-0 right-0 h-1 bg-muted">
                            <div
                              className="h-full bg-primary/60 transition-[width]"
                              style={{ width: `${scrollPct}%` }}
                            />
                          </div>
                          <div className="flex items-center gap-4">
                            <div className="flex-1 min-w-0">
                              <div className="flex items-center gap-2 mb-1">
                                <p className="text-xs text-foreground font-mono uppercase tracking-wider">
                                  {categories.find(c => c.id === essay.category)?.name || essay.category}
                                </p>
                                <span className="text-xs text-muted-foreground">
                                  {scrollPct}% read
                                </span>
                              </div>
                              <h4 className="font-medium text-sm group-hover:text-primary transition-colors truncate">
                                {essay.title}
                              </h4>
                            </div>
                            <div className="flex items-center gap-2">
                              <div className="flex items-center gap-1 text-xs text-muted-foreground">
                                <Clock className="h-3 w-3" />
                                {formatReadingTime(essay.content)}
                              </div>
                              <ChevronRight className="h-4 w-4 text-muted-foreground group-hover:text-foreground/60 group-hover:translate-x-1 transition-[color,transform]" />
                            </div>
                          </div>
                        </motion.div>
                      );
                    })}
                  </div>
                </div>
              );
            })()}

            {/* Search and Filters */}
            <div className="mb-8">
              <div className="relative max-w-md mb-6">
                <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
                <Input aria-label="Search essays..." placeholder="Search essays..."
                  value={searchQuery}
                  onChange={(e) => setSearchQuery(e.target.value)}
                  className="pl-10"
                />
              </div>

              {/* Category Tabs */}
              <div className="flex flex-wrap gap-2">
                {categories.map((category) => (
                  <Button
                    key={category.id}
                    variant={selectedCategory === category.id ? "default" : "outline"}
                    size="sm"
                    onClick={() => setSelectedCategory(category.id)}
                    className="gap-2"
                  >
                    {category.name}
                  </Button>
                ))}
              </div>
            </div>

            {/* Essays Grid */}
            <ScrollReveal delay={0.00}>
            <div className="content-stagger grid md:grid-cols-2 lg:grid-cols-3 gap-6">
              {filteredEssays.map((essay) => {
                const progress = essayProgressMap.get(essay.id);
                const scrollPct = progress?.scrollProgress || 0;
                const isRead = readEssayIds.has(essay.id);

                return (
                  <motion.article
                    key={essay.id}
                    initial={{ opacity: 0, y: 20 }}
                    animate={{ opacity: 1, y: 0 }}
                    className={cn(
                      "group glass-card rounded-xl p-6 hover:border-primary/50 transition-[colors,opacity,transform,shadow] cursor-pointer relative overflow-hidden float-card hover-lift",
                      isRead ? "border-primary/30" : "border-white/10"
                    )}
                    onClick={() => handleSelectEssay(essay)}
                  >
                    {/* Read Badge */}
                    {isRead && (
                      <div className="absolute top-3 right-3">
                        <span className="flex items-center gap-1 text-xs text-foreground bg-muted px-2 py-1 rounded-full">
                          <CheckCircle2 className="h-3 w-3" />
                          Done
                        </span>
                      </div>
                    )}
                    
                    <div className="flex items-start justify-between mb-3">
                      <span className="text-xs text-foreground font-mono uppercase tracking-wider">
                        {categories.find(c => c.id === essay.category)?.name}
                      </span>
                      <span className="flex items-center gap-2 text-xs text-muted-foreground">
                        <span className="flex items-center gap-1">
                          <Clock className="h-3 w-3" />
                          {formatReadingTime(essay.content)}
                        </span>
                        <span className="text-muted-foreground/50">·</span>
                        <span>{getReadingStats(essay.content).wordCount.toLocaleString()} words</span>
                      </span>
                    </div>

                    <h3 className="text-lg font-editorial font-semibold mb-2 group-hover:text-primary transition-colors pr-16">
                      {essay.title}
                    </h3>
                    <p className="text-sm text-muted-foreground mb-4 line-clamp-2">
                      {essay.description}
                    </p>

                    <div className="flex items-center justify-between">
                      <span className="text-xs text-muted-foreground flex items-center gap-1">
                        <BookOpen className="h-3 w-3" />
                        {essay.source}
                      </span>
                      <div className="flex items-center gap-2">
                        {scrollPct > 0 && !isRead && (
                          <span className="text-xs text-muted-foreground font-mono">{scrollPct}%</span>
                        )}
                        <ChevronRight className="h-4 w-4 text-muted-foreground group-hover:text-foreground/60 group-hover:translate-x-1 transition-[color,transform]" />
                      </div>
                    </div>

                    {/* Progress bar at bottom of card */}
                    {scrollPct > 0 && (
                      <div className="absolute bottom-0 left-0 right-0 h-1 bg-muted">
                        <div
                          className={cn(
                            "h-full transition-[width] duration-300",
                            isRead ? "bg-primary" : "bg-primary/60"
                          )}
                          style={{ width: `${scrollPct}%` }}
                        />
                      </div>
                    )}
                  </motion.article>
                );
              })}
            </div>
            </ScrollReveal>

            {filteredEssays.length === 0 && (
              <div className="text-center py-12">
                <div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center mx-auto mb-4">
                  <FileText className="h-8 w-8 text-primary/60" />
                </div>
                <h3 className="text-lg font-medium mb-2">No essays match your filters</h3>
                <p className="text-muted-foreground mb-6 max-w-sm mx-auto">
                  Try a different search term, or click a category above to browse by topic.
                </p>
                <button
                  onClick={() => { setSearchQuery(''); setSelectedCategory('All'); }}
                  className="text-sm text-primary hover:text-primary/80 font-medium transition-colors"
                >
                  Clear all filters and show everything
                </button>
              </div>
            )}
          </>
        )}
      </div>
    </div>
    </PageTransition>
  );
}
