Built in Lith

The source of this site.

lith.codes is 8 .lith files. A prototype of lith build --target static checks them with the lab's parser (lithparse), lowers them to TSX and prerenders every page and llms.txt to plain HTML and text. Client code is limited to the background and small enhancements in /world.js, /home.js and /docs.js.

Measured with o200k_base: the same site took 16,788 tokens of source in Lith against 19,219 hand-written as HTML, CSS, JS and a Python docs generator (-13%). Most of the saving is client JS that became build-time rendering; markup alone is about even, since a static site uses none of tables, forms or server functions, where Lith saves the most.

content.lith

// Content the pages and llms.txt are built from, snapshotted from the spec lab (npm run sync-content).

const SITE = "https://lith.codes"
const CARD = readFileSync("content/lith-card.md", "utf8")
const CARD_TOKENS = 2349 // o200k_base, measured in the spec lab
const SPEC = readFileSync("content/lith-spec.md", "utf8")

docs.lith

// The docs are the spec lab's language card, word for word: the one page a model reads before writing Lith.
// The same text is served raw at /docs/lith-card.md and inside /llms.txt (llms.lith).

// each section may switch the background world (index into DOCS_WORLDS in layout.lith)
const SECTION_WORLD = { "Tables": 1, "Pages and layout": 2, "APIs": 3 }

function Toc(sections, raw: boolean) {
  return <aside class="toc">
    <div class="label">On this page</div>
    <ol>{sections.map((s) => <li><a href={`#${s.slug}`}>{s.title}</a></li>)}</ol>
    {raw ? <div class="raw">
      <a href="/docs/lith-card.md">lith-card.md</a>
      <a href="/llms.txt">llms.txt</a>
      <a href="/llms-full.txt">llms-full.txt</a>
    </div> : null}
  </aside>
}

page "/docs" {
  const DOC = parseMarkdown(CARD)
  return <>
    <title>Lith Docs</title>
    <meta name="description" content="The complete Lith language on one page, written for LLMs first. Copy it into your agent or point it at /llms.txt." />
    <link rel="alternate" type="text/markdown" href="/docs/lith-card.md" title="Lith card (Markdown)" />
    <link rel="alternate" type="text/plain" href="/llms.txt" title="llms.txt" />
    <style>{DOCS_CSS + NO_LIGATURES}</style>

    <header data-world="0">
      <div class="wrap">
        <div>
          <div class="label">Documentation · v0</div>
          <h1>The whole language<br />on <em>one page</em>.</h1>
          <p class="lede">Lith's docs are its language card: one page a model reads once before writing working Lith. In blind tests, a model that had read only this page built 8 of 8 unseen apps.</p>
          <div class="facts">
            <div><b>{CARD_TOKENS.toLocaleString("en-US")}</b>tokens, all of it</div>
            <div><b>{DOC.sections.length}</b>sections</div>
            <div><b>8 / 8</b>blind apps passed</div>
          </div>
        </div>
        <div class="llm">
          <h3>Give it to your agent</h3>
          <p>Paste the card into the context, or point the agent at <code>/llms.txt</code>. The full specification with every diagnostic code is in <code>/llms-full.txt</code>.</p>
          <div class="row">
            <button class="btn primary" id="copy" type="button">Copy card as Markdown</button>
            <a class="btn ghost" href="/llms.txt">llms.txt</a>
            <a class="btn ghost" href="/llms-full.txt">llms-full.txt</a>
          </div>
          <div class="prompt"><span>// example prompt</span>{"\n"}Read {SITE}/llms.txt, then build the app in Lith.</div>
        </div>
      </div>
    </header>

    <div class="wrap layout">
      <Toc sections={DOC.sections} raw={true} />
      <main>
        <div class="intro">{DOC.intro}</div>
        {DOC.sections.map((s) => <section class="doc" id={s.slug} data-world={SECTION_WORLD[s.title]}>
          <h2><a href={`#${s.slug}`}>{s.title}</a></h2>
          {s.blocks}
        </section>)}
      </main>
    </div>
    <script type="text/markdown" id="card-src">{CARD.replaceAll("</script", "<\\/script")}</script>
    <script src="/docs.js" defer></script>
  </>
}

// This site's own source, as an example of Lith that compiles today.
// Source tokens for this site (o200k_base), like for like: src/*.lith + public/home.js + docs.js without this
// page, against the hand-written index.html + docs template + build-docs.py it replaced.
const REWRITE = { handWritten: 19219, lith: 16788 }
page "/source" {
  const files = SOURCES.map((f) => ({ title: f, slug: slug(f), src: readFileSync(`src/${f}`, "utf8") }))
  return <>
    <title>Lith · Source of this site</title>
    <meta name="description" content="The .lith files lith.codes is built from, compiled with the prototype lith build --target static." />
    <style>{DOCS_CSS + NO_LIGATURES}</style>
    <header data-world="0">
      <div class="wrap">
        <div>
          <div class="label">Built in Lith</div>
          <h1>The source of <em>this site</em>.</h1>
          <p class="lede">lith.codes is {files.length} .lith files. A prototype of <code>lith build --target static</code> checks them with the lab's parser (lithparse), lowers them to TSX and prerenders every page and llms.txt to plain HTML and text. Client code is limited to the background and small enhancements in /world.js, /home.js and /docs.js.</p>
          <p class="lede">Measured with o200k_base: the same site took {REWRITE.lith.toLocaleString("en-US")} tokens of source in Lith against {REWRITE.handWritten.toLocaleString("en-US")} hand-written as HTML, CSS, JS and a Python docs generator ({Math.round((REWRITE.lith / REWRITE.handWritten - 1) * 100)}%). Most of the saving is client JS that became build-time rendering; markup alone is about even, since a static site uses none of tables, forms or server functions, where Lith saves the most.</p>
        </div>
      </div>
    </header>
    <div class="wrap layout">
      <Toc sections={files} raw={false} />
      <main>
        {files.map((f, i) => <section class="doc" id={f.slug} data-world={i % 4}>
          <h2><a href={`#${f.slug}`}>{f.title}</a></h2>
          <pre>{Code(f.src)}</pre>
        </section>)}
      </main>
    </div>
    <script src="/docs.js" defer></script>
  </>
}

highlight.lith

// Syntax highlighting for Lith code, done at build time so the HTML already holds the colours.

const DECL_WORDS = new Set(["table", "page", "api", "crud", "layout", "state", "effect", "auth", "own", "function"])
const KEYWORDS = new Set(["const", "let", "return", "if", "else", "async", "await", "try", "catch", "import", "from", "new", "true", "false", "null", "test", "seed", "system", "for", "of"])
const TYPE_WORDS = new Set(["string", "int", "number", "boolean", "Date", "day", "email", "User", "Todo", "Post", "Tag", "Note", "Person", "Html", "Product", "Response"])
const TOKEN = /(\/\/[^\n]*)|("[^"\n]*"|`[^`]*`)|(<\/?)([a-z][a-z0-9]*)|(\b\d+(?:\.\.\d*)?\b)|([A-Za-z_]\w*)/g

function tokens(src: string) {
  const out = []
  let last = 0
  for (const m of src.matchAll(TOKEN)) {
    out.push(src.slice(last, m.index))
    if (m[1]) out.push(<span class="t-com">{m[1]}</span>)
    else if (m[2]) out.push(<span class="t-str">{m[2]}</span>)
    else if (m[3]) out.push(m[3], <span class="t-tag">{m[4]}</span>)
    else if (m[5]) out.push(<span class="t-num">{m[5]}</span>)
    else if (DECL_WORDS.has(m[6])) out.push(<span class="t-decl">{m[6]}</span>)
    else if (KEYWORDS.has(m[6])) out.push(<span class="t-kw">{m[6]}</span>)
    else if (TYPE_WORDS.has(m[6])) out.push(<span class="t-ty">{m[6]}</span>)
    else out.push(m[6])
    last = m.index + m[0].length
  }
  out.push(src.slice(last))
  return out
}

// One <span class="l"> per line (the home page numbers them with CSS counters), joined by real newlines
// so the code copies and reads as code without CSS.
function NumberedCode(src: string, caret: boolean = false) {
  const lines = src.split("\n")
  return lines.map((line, i) => <>{i > 0 ? "\n" : ""}<span class="l">{tokens(line)}{caret && i === lines.length - 1 ? <>{" "}<span class="caret"></span></> : null}</span></>)
}

function Code(src: string) {
  return <>{tokens(src)}</>
}

home.lith

// lith.codes home page. Benchmarks are o200k_base write tokens per task (spec-lab/baseline/multi_stack.json).

const STACKS = [
  { key: "ts", name: "Next.js", sub: "TypeScript", files: 32 },
  { key: "sveltekit", name: "SvelteKit", sub: "TypeScript", files: 34 },
  { key: "laravel", name: "Laravel", sub: "PHP", files: 31 },
  { key: "django", name: "Django", sub: "Python", files: 32 },
  { key: "rails", name: "Rails", sub: "Ruby", files: 37 },
]
const TASKS = [
  { name: "Counter island", id: "T02", li: 114, ts: 229, sveltekit: 168, laravel: 276, django: 293, rails: 297 },
  { name: "Todo CRUD", id: "T03", li: 344, ts: 1193, sveltekit: 1098, laravel: 941, django: 837, rails: 698 },
  { name: "Login & logout", id: "T04", li: 177, ts: 1076, sveltekit: 1135, laravel: 513, django: 600, rails: 606 },
  { name: "JSON CRUD API", id: "T07", li: 38, ts: 744, sveltekit: 681, laravel: 499, django: 331, rails: 391 },
  { name: "Per-user notes", id: "T09", li: 233, ts: 1078, sveltekit: 942, laravel: 743, django: 666, rails: 617 },
  { name: "Optimistic star", id: "T13", li: 263, ts: 630, sveltekit: 623, laravel: 716, django: 586, rails: 615 },
]
const API_FILES = { ts: 5, sveltekit: 5, laravel: 5, django: 5, rails: 4 }

const mean = (key: string) => TASKS.reduce((s, t) => s + t[key], 0) / TASKS.length
const fmt = (n: number) => Math.round(n).toLocaleString("en-US")
const width = (v: number, max: number) => (v / max * 86).toFixed(2)
const STACK_MAX = Math.max(...STACKS.map((s) => mean(s.key)))
const TASK_MAX = Math.max(...TASKS.flatMap((t) => STACKS.map((s) => t[s.key])))
const API = TASKS.find((t) => t.id === "T07")

const HERO_CODE = `table Todo own {
  title: string(1..200)
  done: boolean = false
  created: Date = now()
}

page "/" {
  const todos = db.todo.findMany({ orderBy: [{ done: "asc" }] })
  return <main>
    <form action={addTodo}>
      <input name="title" /><button>Add</button>
    </form>
    <ul>{todos.map(t => <li>
      <input type="checkbox" checked={t.done}
        onChange={() => setDone(t.id, !t.done)} />
      {t.title}
    </li>)}</ul>
  </main>
}

function addTodo(title: string) {
  db.todo.create({ data: { title } })
}

function setDone(id: int, done: boolean) {
  db.todo.update({ where: { id }, data: { done } })
}`

const TODO_CODE = readFileSync("content/todo-example.lith", "utf8").trimEnd()

const FEATURES = [
  { title: "Looks like TypeScript because it is", text: "The TS 5 TSX grammar plus contextual declarations. Models already know it: 64 blind programs had zero syntax-caused errors.", code: <><span class="k">table page api crud layout</span>{"\n"}<span class="k">state effect auth</span></> },
  { title: "Tables with refinement types", text: "Ranges and lengths live in the type and are checked on every write and every form. Migrations for new columns are automatic.", code: <>title: <span class="k">string(1..200)</span>{"\nviews: "}<span class="k">int(0..)</span> = 0</> },
  { title: "Ownership as a keyword", text: <><span class="mono">own</span> scopes every query, update and delete to the logged-in user. Other users' rows act as missing.</>, code: <><span class="k">table</span> Note <span class="k">own</span>{" { body: string }"}</> },
  { title: "Inferred server / client split", text: <>A function that touches <span class="mono">db</span> or <span class="mono">me</span> runs on the server; the browser calls it automatically. No <span class="mono">"use server"</span>, no API glue.</>, code: <>{"<form action={"}<span class="k">addComment</span>{"}>"}</> },
  { title: "Forms that handle themselves", text: "Inputs fill typed parameters by name. Every check runs at once, and failures re-show the form with messages next to each field. It works without JS.", code: <><span class="k">fail</span>{'({ body: "No links" })'}</> },
  { title: "A REST API in one line", text: <>List, get, create, patch and delete with validation, JSON errors and correct status codes. Plain <span class="mono">api</span> routes return any <span class="mono">Response</span>.</>, code: <><span class="k">crud</span> "/api/products" Product</> },
  { title: "Compiled signals, tiny islands", text: <><span class="mono">state</span> and <span class="mono">effect</span> replace hooks. Pages without islands ship 0 kB of JavaScript; the client runtime targets 10 kB gzipped.</>, code: <><span class="k">state</span> starred = task.starred</> },
  { title: "Errors an agent can apply", text: <><span class="mono">lith check</span> returns the first error as JSON with a ready patch, so a fix costs one short round instead of a debugging session.</>, code: <>{'{ "code": '}<span class="k">"E0110"</span>{', "patch": … }'}</> },
  { title: "Native to the web", text: <>Emits standard JS, HTML and CSS on Node 22. Import any npm package; export <span class="mono">.d.ts</span> types so TypeScript can call into Lith.</>, code: <><span class="k">import</span>{" { marked } "}<span class="k">from</span> "marked"</> },
]

const PRINCIPLES = [
  "Retries cost more than keystrokes. Prevent the wrong guess first.",
  "If it looks like JS or React, it behaves like JS or React.",
  "One way to write each thing. Never two.",
  "Always an escape hatch to plain web standards.",
  "Reading code is a cost too. Keep files short and dense.",
]

const NEXT_TREE = [
  ["app/", "", "dir"], ["page.tsx", "server", "ind1"], ["actions.ts", '"use server"', "ind1"], ["todo-controls.tsx", '"use client"', "ind1"],
  ["todos/[id]/", "", "ind1 dir"], ["page.tsx", "server", "ind2"], ["lib/", "", "dir"], ["db.ts", "prisma", "ind1"], ["prisma/", "", "dir"], ["schema.prisma", "schema", "ind1"],
]

// A counter shows its real value in the HTML; home.js animates it up from 0 when it scrolls in.
function Count(value: number, decimals: number = 0, id: string | null = null) {
  return <span id={id} data-count={value.toFixed(decimals)} data-dec={decimals}>{value.toFixed(decimals)}</span>
}

function Stat(value: Html, unit: string, what: string, vs: string, gain: string) {
  return <div class="stat">
    <div class="num">{value}<small>{unit}</small></div>
    <div class="what">{what}</div>
    <div class="vs">{vs} <b>{gain}</b></div>
  </div>
}

function StackRow(name: string, sub: string, value: number, lith: boolean) {
  return <div class={lith ? "row lith" : "row"}>
    <div class="name">{name}<span>{sub}</span></div>
    <div class="bars"><div class={lith ? "bar li" : "bar ts"} data-w={width(value, STACK_MAX)} title={`${name}: ${fmt(value)} tokens per task`}><em>{fmt(value)}</em></div></div>
    <div class={lith ? "x dim" : "x"}>{lith ? "—" : `${(value / mean("li")).toFixed(1)}×`}</div>
  </div>
}

// Rows show Next.js first; data-* holds every stack so home.js can switch without a round trip.
function TaskRow(t) {
  const stacks = Object.fromEntries(STACKS.map((s) => [`data-${s.key}`, t[s.key]]))
  return <div class="row" data-li={t.li} {...stacks}>
    <div class="name">{t.name}<span>{t.id}</span></div>
    <div class="bars">
      <div class="bar ts" data-w={width(t.ts, TASK_MAX)} title={`Next.js · ${t.id}: ${fmt(t.ts)} tokens`}><em>{fmt(t.ts)}</em></div>
      <div class="bar li" data-w={width(t.li, TASK_MAX)} title={`Lith · ${t.id}: ${t.li} tokens`}><em>{t.li}</em></div>
    </div>
    <div class="x">{(t.ts / t.li).toFixed(1)}×</div>
  </div>
}

function CodeWindow(file: string, tag: string, src: string, caret: boolean = false, foot: Html | null = null, extra: string = "") {
  return <div class={`code reveal ${extra}`}>
    <div class="code-head"><div class="dots"><span></span><span></span><span></span></div><span>{file}</span><span>{tag}</span></div>
    <pre>{NumberedCode(src, caret)}</pre>
    {foot}
  </div>
}

page "/" {
  const liMean = mean("li")
  const rails = mean("rails")
  return <>
    <title>Lith</title>
    <meta name="description" content="Lith is a web language built for AI agents: TypeScript, JSX and Prisma-style queries plus five declarations. Ship a correct web app in a quarter of the tokens." />
    <style>{HOME_CSS + NO_LIGATURES}</style>

    <section class="hero" data-world="0">
      <div class="wrap">
        <div class="reveal">
          <span class="tag"><i></i>v0.0 · specification</span>
          <h1>The web language<br />written for <em>agents</em>.</h1>
          <p class="lede">Lith is TypeScript, JSX and Prisma-style queries plus five declarations. An AI agent builds a correct, full-stack web app in about a quarter of the tokens it spends on Next.js.</p>
          <div class="cta">
            <a href="#stats" class="btn primary">See the numbers →</a>
            <a href="#features" class="btn ghost">Features</a>
          </div>
          <div class="hero-meta">
            <div><b>1 file</b>per app</div>
            <div><b>2.3k</b>tokens to learn it</div>
            <div><b>0 kB</b>JS on static pages</div>
          </div>
        </div>
        <CodeWindow file="todos.lith" tag="full-stack" src={HERO_CODE} caret={true} extra="d2" />
      </div>
    </section>

    <section class="stats" id="stats" data-world="1">
      <div class="wrap">
        <div class="reveal">
          <div class="label">01 · Benchmarks</div>
          <h2>Less to write. Less to read. Fewer retries.</h2>
          <p class="lede">Six reference tasks, each written by an expert in Lith and in five popular web stacks: Next.js, SvelteKit, Laravel, Django and Rails. Tokens are counted with the o200k_base tokenizer: what a model actually pays.</p>
        </div>

        <div class="stat-grid reveal">
          <Stat value={Count(rails / liMean, 1)} unit={`–${(mean("ts") / liMean).toFixed(1)}×`} what="fewer tokens to write" vs={`${fmt(liMean)} vs ${fmt(rails)}–${fmt(mean("ts"))} per app`} gain="−64 to −76%" />
          <Stat value={Count(6)} unit="files" what="for all six apps" vs="other stacks: 31–37" gain="−81 to −84%" />
          <Stat value={Count(164)} unit="lines" what="of source in total" vs="Rails 363 · Next.js 611" gain="−55 to −73%" />
          <Stat value={Count(8)} unit="/ 8" what="unseen apps passed" vs="blind model, card only" gain="100%" />
        </div>

        <div class="chart">
          <div class="reveal">
            <div class="chart-head">
              <h3>Mean tokens per task, by stack</h3>
              <div class="legend"><span><i class="ts"></i>Baseline stack</span><span><i class="li"></i>Lith</span></div>
            </div>
            <div>
              {STACKS.map((s) => <StackRow name={s.name} sub={`${s.sub} · ${s.files} files`} value={mean(s.key)} lith={false} />)}
              <StackRow name="Lith" sub="v0.0 · 6 files" value={liMean} lith={true} />
            </div>
          </div>
          <aside class="chart-aside reveal d2">
            <div class="label">Closest stack</div>
            <div class="big">{Count(rails / liMean, 1)}×</div>
            <p>Rails, the leanest stack measured, still writes {(rails / liMean).toFixed(1)}× more. Batteries-included frameworks save tokens on auth, forms and REST, then spend them again on Stimulus or vanilla JS beside every template that needs client interactivity.</p>
          </aside>
        </div>

        <div class="chart">
          <div class="reveal">
            <div class="chart-head">
              <h3>Tokens per task</h3>
              <div class="legend"><span><i class="ts"></i><span id="base-name">Next.js</span></span><span><i class="li"></i>Lith</span></div>
            </div>
            <div class="switch" id="stack-switch" role="group" aria-label="Compare Lith with">
              {STACKS.map((s) => <button type="button" data-key={s.key} data-files={API_FILES[s.key]} aria-pressed={s.key === "ts" ? "true" : "false"}>{s.name}</button>)}
            </div>
            <div id="rows">{TASKS.map((t) => <TaskRow t={t} />)}</div>
          </div>
          <aside class="chart-aside reveal d2">
            <div class="label">Biggest gap</div>
            <div class="big">{Count(API.ts / API.li, 1, "gap")}×</div>
            <p>A full JSON REST API with validation, 404s and status codes. <span id="gap-text">{API_FILES.ts} files of Next.js</span> become one <span class="mono">crud</span> line and a table.</p>
            <div class="formula">
              Lith is designed against one cost:<br />
              <b>C</b> = T<sub>read</sub> + T<sub>write</sub> + Σ (T<sub>error</sub> + T<sub>fix</sub>)<br />
              every token an agent reads, writes and spends on retries.
            </div>
          </aside>
        </div>
        <p class="note reveal">Write tokens are the mean of 6 expert solutions per stack. Files and lines count everything written for a task, starting from each framework's standard scaffold. The blind result: a fresh model that had only the 2,349-token Lith card built 4 unseen apps, 2 testers each, 8 of 8 passing after compiler-guided fixes.</p>
      </div>
    </section>

    <section class="compare" data-world="3">
      <div class="wrap">
        <div class="reveal">
          <div class="label">02 · Same app, side by side</div>
          <h2>One file instead of a framework's worth.</h2>
          <p class="lede">A todo app with per-user data, validation, a detail page and a 404. In Lith, the schema, pages, server logic and the server/client split all live together, and the compiler infers which code runs where.</p>
        </div>
        <div class="cmp">
          <div class="tree reveal">
            <h4>TypeScript / Next.js</h4>
            <ul>{NEXT_TREE.map(([file, note, cls]) => <li class={cls}><span>{file}</span><span>{note}</span></li>)}</ul>
            <div class="sum"><span>6 files · 146 lines</span><span>1,193 tok</span></div>
          </div>
          <CodeWindow file="T03_app.lith" tag="Lith" src={TODO_CODE} extra="d1"
            foot={<div class="code-foot"><span>1 file · {TODO_CODE.split("\n").length} lines</span><span>344 tok · 3.5× fewer</span></div>} />
        </div>
      </div>
    </section>

    <section class="features" id="features" data-world="4">
      <div class="wrap">
        <div class="reveal">
          <div class="label">03 · Highlights</div>
          <h2>Every feature had to earn its tokens.</h2>
          <p class="lede">Lith came out of a measured design lab: four competing designs, blind tests with models that had never seen the language, and a rule that every feature must lower total cost.</p>
        </div>
        <div class="feat-grid reveal">
          {FEATURES.map((f, i) => <article class="feat">
            <span class="idx">/{String(i + 1).padStart(2, "0")}</span>
            <h3>{f.title}</h3>
            <p>{f.text}</p>
            <code>{f.code}</code>
          </article>)}
        </div>
      </div>
    </section>

    <section class="closing" data-world="5">
      <div class="wrap">
        <div class="reveal">
          <div class="label">04 · Principles</div>
          <h2>Built on what the measurements said, not on taste.</h2>
        </div>
        <ul class="principles reveal">
          {PRINCIPLES.map((p, i) => <li><span>{["i", "ii", "iii", "iv", "v"][i]}</span>{p}</li>)}
        </ul>
        <p class="sources reveal">Token counts: o200k_base · 6 measured tasks · 5 baseline stacks · blind validation n = 8</p>
      </div>
    </section>
    <script src="/home.js" defer></script>
  </>
}

layout.lith

// Imports are shared by every .lith file (one namespace); the docs and source pages read files at build time.
import { readdirSync, readFileSync } from "node:fs"

// The source files this site is built from, counted at build time for the footer.
const SOURCES = readdirSync("src").filter((f) => f.endsWith(".lith")).sort()

// Background worlds rendered for the Álmodozó music video; sections pick one by index (data-world).
const HOME_WORLDS = "indigo-twist,aurora-silk,ember-silk,jade-spire,paper-boats,rose-twist"
const DOCS_WORLDS = "nebula-veil,sapphire-veil,golden-sea,celadon-orbs"

const GITHUB_ICON = "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"

layout {
  const home = path === "/"
  return <html lang="en">
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
    <style>{BASE_CSS}</style>
    <noscript><style>{".reveal { opacity: 1; transform: none; }"}</style></noscript>

    <div class="world" aria-hidden="true" data-worlds={home ? HOME_WORLDS : DOCS_WORLDS} data-dim={home ? "0.62" : "0.5"}><canvas id="world"></canvas></div>
    <div class="scrim" aria-hidden="true"></div>

    <nav>
      <div class="wrap">
        <a href="/" class="brand">Lith</a>
        <div class="nav-links">
          <a class="docs-link" href="/docs" aria-current={path === "/docs" ? "page" : null}>Docs</a>
          {/* Replace with the real repository URL */}
          <a class="gh" href="https://github.com/" target="_blank" rel="noopener">
            <svg viewBox="0 0 16 16" aria-hidden="true"><path d={GITHUB_ICON} /></svg>
            GitHub
          </a>
        </div>
      </div>
    </nav>

    {children}

    <footer class="site">
      <div class="wrap">
        <span>Lith · v0 specification</span>
        <span class="built"><i></i><span>This site is <b>built in Lith</b>: {SOURCES.length} .lith files compiled with lith build --target static. <a href="/source">Read the source</a></span></span>
      </div>
    </footer>
    <script src="/world.js" defer></script>
  </html>
}

llms.lith

// Plain-text docs for agents (https://llmstxt.org): the whole card in /llms.txt, the card plus the language
// sections of the spec (§2–§10) in /llms-full.txt, and the raw card for the docs page's links.

const SPEC_LANGUAGE = SPEC.match(/^## 2\. [\s\S]*?(?=^## 11\. )/m)?.[0] ?? SPEC
const CARD_SECTION = CARD.replace("# Lith card\n", "## Language card\n")
const LLMS_HEAD = `# Lith

> Lith is a web language for AI agents: TypeScript + JSX + Prisma-style queries plus the declarations
> \`table page api crud layout\` and \`state\`/\`effect\`/\`auth\`. One global namespace, server/client split
> inferred, compiles to standard JS/HTML/CSS on Node. Status: v0 specification.

`

const text = (body: string, type: string = "text/plain") => new Response(body, { headers: { "Content-Type": `${type}; charset=utf-8` } })

api GET "/llms.txt" {
  return text(LLMS_HEAD
    + "Everything below is the complete language card: read it once and you can write Lith.\n"
    + `Full specification (types, effects, codegen, every diagnostic code): ${SITE}/llms-full.txt\n\n`
    + CARD_SECTION)
}

api GET "/llms-full.txt" {
  return text(LLMS_HEAD
    + "This file holds the language card followed by the full specification.\n\n"
    + CARD_SECTION
    + "\n\n# Lith specification (sections 2-10)\n\n" + SPEC_LANGUAGE)
}

api GET "/docs/lith-card.md" {
  return text(CARD, "text/markdown")
}

markdown.lith

// The markdown subset the Lith card uses: ## headings, paragraphs, "- " bullets (continued by indented
// lines), ``` fences and `code`. Returns the text before the first heading and one entry per section.

function slug(s: string) {
  return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")
}

function inline(s: string) {
  return s.split(/(`[^`]*`)/).map((p) => p.length > 1 && p.startsWith("`") && p.endsWith("`") ? <code>{p.slice(1, -1)}</code> : p)
}

function parseMarkdown(md: string) {
  const intro = []
  const sections = []
  let blocks = intro
  let para = []
  let items = []
  const flush = () => {
    if (para.length) blocks.push(<p>{inline(para.join(" "))}</p>)
    if (items.length) blocks.push(<ul>{items.map((it) => <li>{inline(it.join(" "))}</li>)}</ul>)
    para = []
    items = []
  }
  const lines = md.split("\n")
  for (let i = 0; i < lines.length; i++) {
    const ln = lines[i]
    if (ln.startsWith("```")) {
      flush()
      const code = []
      i++
      while (i < lines.length && !lines[i].startsWith("```")) code.push(lines[i++])
      blocks.push(<pre>{Code(code.join("\n"))}</pre>)
    } else if (ln.startsWith("# ")) {
      // the page has its own title
    } else if (ln.startsWith("## ")) {
      flush()
      const title = ln.slice(3).trim()
      blocks = []
      sections.push({ title, slug: slug(title), blocks })
    } else if (ln.startsWith("- ")) {
      if (para.length) flush()
      items.push([ln.slice(2).trim()])
    } else if (ln.startsWith("  ") && items.length && ln.trim()) {
      items[items.length - 1].push(ln.trim())
    } else if (!ln.trim()) {
      flush()
    } else {
      if (items.length) flush()
      para.push(ln.trim())
    }
  }
  flush()
  return { intro, sections }
}

styles.lith

// Stylesheets as strings: Lith emits <style> children raw (spec §7), so each page carries its own CSS.

const BASE_CSS = `
  :root {
    color-scheme: dark;
    --bg: #0a0d10;
    --bg-raised: rgba(14, 18, 22, .78);   /* panels over the moving background */
    --panel: rgba(10, 13, 16, .62);
    --ink: #eceff2;
    --ink-2: #b4bcc4;
    --ink-3: #7d8790;
    --line: rgba(255, 255, 255, .12);
    --line-2: rgba(255, 255, 255, .07);
    --accent: #8fbcbb;        /* frost */
    --base: #5b646c;          /* baseline stack bars */
    --code-bg: rgba(9, 12, 15, .9);
    --code-ink: #d6dbe0;
    --code-dim: #66727d;
    --code-kw: #8fbcbb;
    --code-str: #c9b48a;
    --code-tag: #a3b9d1;
    --code-ty: #b9a7c9;
    --code-num: #d4a28c;
    --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
    --sans: "Inter Tight", system-ui, -apple-system, sans-serif;
    --wrap: 1120px;
  }

  * { box-sizing: border-box; margin: 0; padding: 0; }
  html { scroll-behavior: smooth; background: var(--bg); }
  body {
    background: transparent; color: var(--ink);   /* html paints --bg; the world canvas sits between */
    font: 400 16px/1.6 var(--sans);
    -webkit-font-smoothing: antialiased;
    overflow-x: hidden;
  }
  a { color: inherit; text-decoration: none; }
  .wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 32px; }
  .mono { font-family: var(--mono); }

  /* One fixed WebGL canvas behind everything; a CSS crossfade stands in without WebGL. */
  .world { position: fixed; inset: 0; z-index: -2; pointer-events: none; }
  .world canvas, .world .fallback { position: absolute; inset: 0; width: 100%; height: 100%; }
  .world .fallback img {
    position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
    opacity: 0; transition: opacity 1.2s ease; filter: brightness(.55) saturate(1.05);
  }
  .world .fallback img.on { opacity: 1; }
  .scrim {
    position: fixed; inset: 0; z-index: -1; pointer-events: none;
    background:
      linear-gradient(90deg, rgba(7, 9, 12, .78) 0%, rgba(7, 9, 12, .5) 50%, rgba(7, 9, 12, .3) 100%),
      linear-gradient(180deg, rgba(7, 9, 12, .35) 0%, transparent 30%, transparent 70%, rgba(7, 9, 12, .5) 100%);
  }

  nav {
    position: sticky; top: 0; z-index: 10;
    background: rgba(8, 10, 13, .76);   /* no backdrop blur: it re-blurs the moving canvas every frame */
    border-bottom: 1px solid var(--line);
  }
  nav .wrap { height: 64px; display: flex; align-items: center; justify-content: space-between; }
  .brand { font-weight: 600; font-size: 20px; letter-spacing: -0.02em; }
  .nav-links { display: flex; align-items: center; gap: 8px; }
  .docs-link {
    display: inline-flex; align-items: center; height: 38px; padding: 0 14px;
    font: 500 13px/1 var(--mono); letter-spacing: 0.02em; color: var(--ink-2);
    border: 1px solid transparent; border-radius: 2px; transition: color .2s, border-color .2s;
  }
  .docs-link:hover, .docs-link[aria-current] { color: var(--ink); border-color: var(--line); }
  .gh {
    display: inline-flex; align-items: center; gap: 10px;
    height: 38px; padding: 0 16px;
    border: 1px solid var(--ink); border-radius: 2px;
    font: 500 13px/1 var(--mono); letter-spacing: 0.02em;
    transition: background .2s, color .2s;
  }
  .gh:hover { background: var(--ink); color: var(--bg); }
  .gh svg { width: 16px; height: 16px; fill: currentColor; }

  .label {
    font: 500 12px/1 var(--mono); letter-spacing: 0.12em; text-transform: uppercase;
    color: var(--ink-3); display: flex; align-items: center; gap: 12px;
  }
  .label::before { content: ""; width: 24px; height: 1px; background: var(--ink-3); }

  .t-kw { color: var(--code-kw); }
  .t-decl { color: var(--code-kw); font-weight: 500; }
  .t-str { color: var(--code-str); }
  .t-com { color: var(--code-dim); font-style: italic; }
  .t-tag { color: var(--code-tag); }
  .t-ty { color: var(--code-ty); }
  .t-num { color: var(--code-num); }

  footer.site { border-top: 1px solid var(--line); background: rgba(8, 10, 13, .6); padding: 30px 0 38px; }
  footer.site .wrap { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; font: 400 12px/1.6 var(--mono); color: var(--ink-3); }
  footer.site a { color: var(--ink-2); border-bottom: 1px solid var(--line); }
  footer.site a:hover { color: var(--accent); border-color: var(--accent); }
  .built { display: inline-flex; align-items: center; gap: 10px; }
  .built b { color: var(--ink); font-weight: 500; }
  .built i { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }

  @media (max-width: 960px) {
    .scrim { background: rgba(7, 9, 12, .6); }
  }
  @media (max-width: 600px) {
    .wrap { padding: 0 16px; }
  }
`

// Last in every page's CSS, so no `font:` shorthand after it resets it: show != and => as typed.
const NO_LIGATURES = `
  pre, code, .mono { font-variant-ligatures: none; font-feature-settings: "calt" 0, "liga" 0; }
`

const HOME_CSS = `
  /* ---------- Shared ---------- */
  section { border-bottom: 1px solid var(--line); }
  h2 {
    font-weight: 500; font-size: clamp(30px, 4vw, 46px); line-height: 1.08;
    letter-spacing: -0.03em; margin-top: 20px; max-width: 18ch;
  }
  .lede { color: var(--ink-2); font-size: 18px; max-width: 56ch; margin-top: 18px; }
  .reveal { opacity: 0; transform: translateY(14px); transition: opacity .8s ease, transform .8s ease; }
  .reveal.in { opacity: 1; transform: none; }

  /* ---------- Hero ---------- */
  .hero { position: relative; overflow: hidden; }
  .hero::before {
    content: ""; position: absolute; inset: 0; pointer-events: none;
    background-image:
      linear-gradient(var(--line-2) 1px, transparent 1px),
      linear-gradient(90deg, var(--line-2) 1px, transparent 1px);
    background-size: 56px 56px;
    mask-image: radial-gradient(ellipse 70% 80% at 70% 40%, #000 20%, transparent 75%);
    -webkit-mask-image: radial-gradient(ellipse 70% 80% at 70% 40%, #000 20%, transparent 75%);
  }
  .hero .wrap {
    position: relative; display: grid; grid-template-columns: 1fr 1.05fr; gap: 64px;
    align-items: center; padding-top: 96px; padding-bottom: 104px;
  }
  .tag {
    display: inline-flex; align-items: center; gap: 10px;
    font: 500 12px/1 var(--mono); color: var(--ink-2);
    border: 1px solid var(--line); background: var(--bg-raised);
    padding: 8px 12px; border-radius: 2px;
  }
  .tag i { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); animation: pulse 2.4s infinite; }
  @keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: .25 } }
  h1 {
    font-weight: 500; font-size: clamp(44px, 6.4vw, 80px); line-height: 0.98;
    letter-spacing: -0.045em; margin-top: 28px;
  }
  h1 em { font-style: normal; color: var(--accent); }
  .hero p.lede { font-size: 19px; }
  .cta { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 36px; }
  .btn {
    display: inline-flex; align-items: center; gap: 10px; height: 46px; padding: 0 22px;
    font: 500 14px/1 var(--mono); border-radius: 2px; transition: transform .2s, background .2s;
  }
  .btn.primary { background: var(--ink); color: var(--bg); }
  .btn.primary:hover { transform: translateY(-1px); }
  .btn.ghost { border: 1px solid var(--line); color: var(--ink-2); background: var(--bg-raised); }
  .btn.ghost:hover { border-color: var(--ink-3); color: var(--ink); }
  .hero-meta { display: flex; gap: 32px; margin-top: 48px; padding-top: 24px; border-top: 1px solid var(--line); }
  .hero-meta div { font: 400 12px/1.5 var(--mono); color: var(--ink-3); }
  .hero-meta b { display: block; font: 500 22px/1.2 var(--sans); color: var(--ink); letter-spacing: -0.02em; }

  /* ---------- Code window ---------- */
  .code {
    background: var(--code-bg); color: var(--code-ink); border-radius: 4px;
    box-shadow: 0 30px 60px -30px rgba(10, 20, 30, .45), 0 0 0 1px rgba(255,255,255,.04);
    overflow: hidden; min-width: 0;
  }
  .code-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: 12px 16px; border-bottom: 1px solid rgba(255,255,255,.06);
    font: 400 12px/1 var(--mono); color: var(--code-dim);
  }
  .dots { display: flex; gap: 6px; }
  .dots span { width: 9px; height: 9px; border-radius: 50%; background: #2c353d; }
  pre {
    font: 400 13px/1.7 var(--mono); padding: 18px 20px 18px 0; overflow-x: auto; counter-reset: ln;
    tab-size: 2;
  }
  pre .l { white-space: pre; }  /* inline: the newlines between lines do the breaking */
  pre .l::before {
    counter-increment: ln; content: counter(ln);
    display: inline-block; width: 44px; padding-right: 16px; text-align: right;
    color: #3a444d; user-select: none;
  }
  .caret { display: inline-block; width: 7px; height: 15px; background: var(--code-kw); vertical-align: -3px; animation: blink 1.1s steps(1) infinite; }
  @keyframes blink { 50% { opacity: 0 } }

  /* ---------- Stats ---------- */
  .stats { padding: 112px 0; }
  .stat-grid {
    display: grid; grid-template-columns: repeat(4, 1fr); margin-top: 64px;
    border-top: 1px solid var(--line); border-left: 1px solid var(--line);
  }
  .stat {
    padding: 28px 24px 30px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line);
    background: var(--bg-raised);
  }
  .stat .num {
    font-weight: 500; font-size: clamp(40px, 4.6vw, 60px); line-height: 1; letter-spacing: -0.04em;
    font-variant-numeric: tabular-nums;
  }
  .stat .num small { font-size: .45em; color: var(--ink-3); margin-left: 4px; letter-spacing: 0; }
  .stat .what { margin-top: 14px; font-weight: 500; }
  .stat .vs { margin-top: 6px; font: 400 12px/1.5 var(--mono); color: var(--ink-3); }
  .stat .vs b { color: var(--accent); font-weight: 500; }

  .chart { margin-top: 72px; display: grid; grid-template-columns: 1fr 300px; gap: 56px; align-items: start; }
  .chart-head { display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: 12px; margin-bottom: 26px; }
  .chart-head h3 { font-weight: 500; font-size: 20px; letter-spacing: -0.02em; }
  .legend { display: flex; gap: 20px; font: 400 12px/1 var(--mono); color: var(--ink-2); }
  .legend span { display: inline-flex; align-items: center; gap: 8px; }
  .legend i { width: 10px; height: 10px; display: inline-block; }
  .row { display: grid; grid-template-columns: 170px 1fr 64px; align-items: center; gap: 18px; padding: 12px 0; border-top: 1px solid var(--line-2); }
  .row:last-child { border-bottom: 1px solid var(--line-2); }
  .row .name { font-size: 14px; }
  .row .name span { display: block; font: 400 11px/1.4 var(--mono); color: var(--ink-3); }
  .bars { display: grid; gap: 5px; }
  .bar { height: 10px; width: 0; transition: width 1.4s cubic-bezier(.2,.7,.1,1); position: relative; }
  .bar.ts { background: var(--base); }
  .bar.li { background: var(--accent); }
  .bar em {
    position: absolute; left: calc(100% + 8px); top: 50%; transform: translateY(-50%);
    font: 400 11px/1 var(--mono); font-style: normal; color: var(--ink-3); white-space: nowrap;
    opacity: 0; transition: opacity .4s 1.1s;
  }
  .in .bar em { opacity: 1; }
  .row .x { font: 500 14px/1 var(--mono); color: var(--accent); text-align: right; font-variant-numeric: tabular-nums; }
  .chart-aside { border: 1px solid var(--line); background: var(--bg-raised); padding: 28px; }
  .chart-aside .big { font-size: 64px; font-weight: 500; letter-spacing: -0.05em; line-height: 1; color: var(--accent); font-variant-numeric: tabular-nums; }
  .chart-aside p { color: var(--ink-2); font-size: 14px; margin-top: 14px; }
  .formula { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line); font: 400 12px/1.7 var(--mono); color: var(--ink-2); }
  .formula b { color: var(--ink); font-weight: 500; }
  .note { margin-top: 28px; font: 400 12px/1.6 var(--mono); color: var(--ink-3); max-width: 80ch; }
  .row.lith .name { color: var(--accent); font-weight: 500; }
  .row .x.dim { color: var(--ink-3); }
  .switch { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 18px; }
  .switch button {
    font: 500 12px/1 var(--mono); color: var(--ink-2); background: var(--bg-raised);
    border: 1px solid var(--line); border-radius: 2px; padding: 8px 12px; cursor: pointer;
    transition: background .2s, color .2s, border-color .2s;
  }
  .switch button:hover { border-color: var(--ink-3); color: var(--ink); }
  .switch button[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--bg); }
  .switch button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

  /* ---------- Compare ---------- */
  .compare { padding: 112px 0; }
  .cmp { display: grid; grid-template-columns: 1fr 1.6fr; gap: 24px; margin-top: 56px; align-items: stretch; }
  .tree { border: 1px solid var(--line); background: var(--bg-raised); padding: 24px 24px 20px; display: flex; flex-direction: column; }
  .tree h4, .cmp .code-title { font: 500 12px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3); }
  .tree ul { list-style: none; margin-top: 18px; font: 400 13px/2 var(--mono); color: var(--ink-2); flex: 1; }
  .tree li { display: flex; justify-content: space-between; gap: 12px; }
  .tree li span:last-child { color: var(--ink-3); }
  .tree .ind1 { padding-left: 16px; } .tree .ind2 { padding-left: 32px; } .tree .ind3 { padding-left: 48px; }
  .tree .dir { color: var(--ink-3); }
  .tree .sum { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line); display: flex; justify-content: space-between; font: 500 13px/1 var(--mono); }
  .cmp .code pre { max-height: 540px; }
  .code-foot { display: flex; justify-content: space-between; padding: 12px 16px; border-top: 1px solid rgba(255,255,255,.06); font: 500 12px/1 var(--mono); color: var(--code-kw); }

  /* ---------- Features ---------- */
  .features { padding: 112px 0; }
  .feat-grid {
    display: grid; grid-template-columns: repeat(3, 1fr); margin-top: 64px;
    border-top: 1px solid var(--line); border-left: 1px solid var(--line);
    background: var(--panel);
  }
  .feat {
    padding: 32px 28px 30px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line);
    display: flex; flex-direction: column; gap: 14px; transition: background .3s;
  }
  .feat:hover { background: rgba(20, 26, 31, .7); }
  .feat .idx { font: 400 12px/1 var(--mono); color: var(--ink-3); }
  .feat h3 { font-weight: 500; font-size: 19px; letter-spacing: -0.02em; line-height: 1.25; }
  .feat p { color: var(--ink-2); font-size: 14.5px; line-height: 1.6; }
  .feat code {
    margin-top: auto; display: block; font: 400 12px/1.6 var(--mono); color: var(--ink);
    background: var(--bg); border: 1px solid var(--line); padding: 10px 12px; border-radius: 2px;
    white-space: pre; overflow-x: auto;
  }
  .feat code .k { color: var(--accent); font-weight: 500; }

  /* ---------- Closing ---------- */
  .closing { padding: 120px 0; text-align: left; }
  .closing .wrap { display: grid; grid-template-columns: 1.2fr 1fr; gap: 64px; align-items: end; }
  .principles { list-style: none; border-top: 1px solid var(--line); background: var(--panel); padding: 0 20px; }
  .principles li { display: grid; grid-template-columns: 36px 1fr; padding: 14px 0; border-bottom: 1px solid var(--line); font-size: 15px; color: var(--ink-2); }
  .principles li span { font: 400 12px/1.9 var(--mono); color: var(--ink-3); }

  /* delays, legend swatches and spacing that used to be inline style attributes (E0606) */
  .d1 { transition-delay: .1s; }
  .d2 { transition-delay: .15s; }
  .legend i.ts { background: var(--base); }
  .legend i.li { background: var(--accent); }
  .chart-aside .big { margin-top: 18px; }
  .sources { grid-column: 1 / -1; font: 400 12px/1.6 var(--mono); color: var(--ink-3); }

  /* ---------- Responsive ---------- */
  @media (max-width: 960px) {
    .hero .wrap, .chart, .cmp, .closing .wrap { grid-template-columns: 1fr; gap: 40px; }
    .stat-grid { grid-template-columns: repeat(2, 1fr); }
    .feat-grid { grid-template-columns: repeat(2, 1fr); }
    .hero .wrap { padding-top: 64px; padding-bottom: 72px; }
  }
  @media (max-width: 600px) {
    .stat-grid, .feat-grid { grid-template-columns: 1fr; }
    .row { grid-template-columns: 1fr 48px; gap: 8px 12px; }
    .row .name { grid-column: 1 / -1; }
    .row .bars { grid-column: 1; }
    .bar em { display: none; }
    .stats, .compare, .features, .closing { padding: 80px 0; }
    .hero-meta { gap: 20px; flex-wrap: wrap; }
    pre { font-size: 12px; }
    pre .l::before { width: 32px; padding-right: 12px; }
  }
  @media (prefers-reduced-motion: reduce) {
    .reveal { opacity: 1; transform: none; transition: none; }
    .bar { transition: none; }
    .tag i, .caret { animation: none; }
  }
`

const DOCS_CSS = `
  /* docs: denser panels, wider column, anchors clear the sticky nav */
  :root { --panel: rgba(10, 13, 16, .82); --wrap: 1180px; }
  html { scroll-padding-top: 88px; }
  .scrim { background: linear-gradient(90deg, rgba(7, 9, 12, .7) 0%, rgba(7, 9, 12, .45) 60%, rgba(7, 9, 12, .3) 100%); }
  /* ---------- Header ---------- */
  header { padding: 88px 0 56px; }
  header .wrap { display: grid; grid-template-columns: 1.1fr 1fr; gap: 56px; align-items: end; }
  h1 { font-weight: 500; font-size: clamp(40px, 5.6vw, 68px); line-height: 1; letter-spacing: -0.04em; margin-top: 22px; }
  h1 em { font-style: normal; color: var(--accent); }
  .lede { color: var(--ink-2); font-size: 18px; max-width: 54ch; margin-top: 20px; }
  .facts { display: flex; gap: 32px; margin-top: 32px; padding-top: 20px; border-top: 1px solid var(--line); }
  .facts div { font: 400 12px/1.5 var(--mono); color: var(--ink-3); }
  .facts b { display: block; font: 500 22px/1.2 var(--sans); color: var(--ink); letter-spacing: -0.02em; }

  .llm { border: 1px solid var(--line); background: var(--panel); padding: 26px; }
  .llm h3 { font-weight: 500; font-size: 18px; letter-spacing: -0.01em; }
  .llm p { color: var(--ink-2); font-size: 14px; margin-top: 8px; }
  .llm .row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; }
  .btn {
    display: inline-flex; align-items: center; gap: 8px; height: 40px; padding: 0 16px;
    font: 500 13px/1 var(--mono); border-radius: 2px; cursor: pointer; border: 0;
    transition: transform .2s, border-color .2s, color .2s;
  }
  .btn.primary { background: var(--ink); color: var(--bg); }
  .btn.primary:hover { transform: translateY(-1px); }
  .btn.ghost { background: transparent; border: 1px solid var(--line); color: var(--ink-2); }
  .btn.ghost:hover { border-color: var(--ink-3); color: var(--ink); }
  .prompt {
    margin-top: 18px; font: 400 12.5px/1.6 var(--mono); color: var(--code-ink);
    background: var(--code-bg); border: 1px solid var(--line-2); padding: 12px 14px; border-radius: 2px;
    white-space: pre-wrap; word-break: break-word;
  }
  .prompt span { color: var(--code-dim); }

  /* ---------- Body ---------- */
  .layout { display: grid; grid-template-columns: 210px minmax(0, 1fr); gap: 40px; padding-bottom: 96px; }
  .toc { position: sticky; top: 96px; align-self: start; }
  .toc ol { list-style: none; margin-top: 16px; border-left: 1px solid var(--line); }
  .toc a {
    display: block; padding: 6px 0 6px 16px; margin-left: -1px; border-left: 1px solid transparent;
    font-size: 14px; color: var(--ink-3); transition: color .2s, border-color .2s;
  }
  .toc a:hover { color: var(--ink); }
  .toc a.on { color: var(--ink); border-left-color: var(--accent); }
  .toc .raw { margin-top: 22px; display: grid; gap: 6px; font: 400 12px/1.5 var(--mono); }
  .toc .raw a { padding: 0; border: 0; font: inherit; color: var(--ink-3); }
  .toc .raw a:hover { color: var(--accent); }

  .intro { background: var(--panel); border: 1px solid var(--line); padding: 26px 30px; margin-bottom: 20px; }
  .intro p { color: var(--ink); font-size: 17px; overflow-wrap: anywhere; }
  .doc { background: var(--panel); border: 1px solid var(--line); padding: 30px 30px 26px; margin-bottom: 20px; }
  .doc h2 { font-weight: 500; font-size: 26px; letter-spacing: -0.02em; margin-bottom: 14px; }
  .doc h2 a:hover { color: var(--accent); }
  .doc p, .doc li { color: var(--ink-2); font-size: 15.5px; overflow-wrap: anywhere; }  /* long inline code may wrap on phones */
  .doc p + p, .doc p + ul, .doc ul + p, .doc pre + p, .doc pre + ul { margin-top: 14px; }
  .doc ul { list-style: none; display: grid; gap: 10px; }
  .doc li { position: relative; padding-left: 18px; }
  .doc li::before { content: ""; position: absolute; left: 0; top: .78em; width: 8px; height: 1px; background: var(--accent); }
  :not(pre) > code {
    font: 400 .86em/1 var(--mono); color: var(--ink);
    background: rgba(255, 255, 255, .07); border: 1px solid var(--line-2); padding: 2px 5px; border-radius: 2px;
    white-space: normal;
  }
  pre {
    font: 400 13px/1.7 var(--mono); color: var(--code-ink); background: var(--code-bg);
    border: 1px solid var(--line-2); border-radius: 3px; padding: 16px 18px; margin: 14px 0 4px;
    overflow-x: auto; tab-size: 2; white-space: pre;
  }


  @media (max-width: 960px) {
    header .wrap { grid-template-columns: 1fr; gap: 32px; }
    .layout { grid-template-columns: minmax(0, 1fr); }  /* let wide code blocks scroll instead of widening the page */
    .toc { display: none; }
    .scrim { background: rgba(7, 9, 12, .55); }
  }
  @media (max-width: 600px) {
    header { padding: 56px 0 40px; }
    .doc, .intro { padding: 22px 18px; }
    pre { font-size: 12px; padding: 14px; }
    .facts { gap: 20px; flex-wrap: wrap; }
  }
`