refactor(docs): rebuild the site around reading, not launching

The generated site had the shape of a product launch page: a near-black navy
ground with mint/coral radial glows, a gradient-clipped headline, a glowing
pill badge, nine emoji feature cards and scroll-reveal animations. None of it
told a reader anything about the language.

It is now typographic and light-first — a warm paper ground, a serif display
face, one ink-blue accent used only where it means something, and rules
instead of floating cards. Colour is reserved for code. Dark mode is the same
design with the ground inverted, defined once as tokens under a single
prefers-color-scheme block.

Structurally:

- base.css holds the tokens and shared chrome; site.css and docs.css hold what
  is specific to the landing page and the reference pages. They ship as linked
  files rather than being inlined into all 900+ pages, which takes the site
  from 16 MB to 5 MB and means a design change no longer needs a regenerate to
  be seen.
- api.css was dead — the generator never referenced it, rendering the API index
  with item.css — and is gone. docs.css replaces item.css and covers all four
  reference page kinds.
- Grids draw their separators as cell borders instead of bleeding a ruled
  background through gaps, so a final row with fewer cards than columns stops
  cleanly instead of leaving a grey hole. The feature card count is not a
  multiple of the column count at any breakpoint.
- Inline code loses its tinted chip; in a language reference, a box behind
  every keyword turns a paragraph into confetti.
- Fonts are the platform's own, so the site makes no webfont request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-05 01:47:36 +03:00
parent 583783449a
commit 2f3833a295
12 changed files with 1046 additions and 571 deletions

View file

@ -6,6 +6,12 @@
"runtimeExecutable": "python3",
"runtimeArgs": ["-m", "http.server", "8123", "-d", "build/web"],
"port": 8123
},
{
"name": "ludic-docs",
"runtimeExecutable": "python3",
"runtimeArgs": ["-m", "http.server", "8124", "-d", "build/pages"],
"port": 8124
}
]
}

22
changes/site-redesign.md Normal file
View file

@ -0,0 +1,22 @@
bump: minor
type: docs
The generated site is redesigned around reading rather than launching: a warm
paper ground with a serif display face, one ink-blue accent, and rules instead
of floating cards. Colour is reserved for code. Dark mode is the same design
with the ground inverted, driven entirely by tokens under one
`prefers-color-scheme` block, and the landing page's scroll-reveal animations,
gradient headline, glowing badge and emoji feature icons are gone.
- Stylesheets are linked files (`base.css` + `site.css`/`docs.css`) instead of
being inlined into all 900+ pages, which cuts the published site from 16 MB to
5 MB and means a design change no longer requires regenerating to be seen.
- Fonts are the platform's own; the site makes no webfont request.
- `api.css` was dead — the generator never referenced it — and is removed along
with `item.css`, which `docs.css` replaces.
- A page no longer flashes its own title on every plain visit; only a deep link
highlights its target, and under `prefers-reduced-motion` the highlight no
longer stays on the element permanently.
- The copy leads with what is verifiable — ahead-of-time compiled, an ECS in the
syntax, deterministic fixed-point, no C in a build — and the "get started"
steps now begin with the clang-plus-seed bootstrap, without which `bin/x` does
not exist on a clean checkout.

View file

@ -1,108 +1,233 @@
{
"brand": "Ludic",
"tagline": "The opinionated, compiled language for 2D games.",
"tagline": "A compiled language for 2D games, with the entity system in the syntax.",
"repo_url": "https://git.workshopsoft.io/workshopsoft/ludic",
"meta": {
"title": "Ludic — the opinionated game language",
"description": "Ludic is an opinionated, compiled language for 2D games. The entity system, rendering, named colors, input, deterministic math and save/load are part of the language — so you build a game, not a framework.",
"og_title": "Ludic — the opinionated game language",
"og_description": "An opinionated, compiled language for 2D games. The ECS, rendering, colors, input and save/load are built in — build a game, not a framework."
"title": "Ludic — a compiled game language",
"description": "Ludic compiles ahead of time to a native binary. The entity-component system is part of the syntax, the runtime is deterministic fixed-point, and the compiler is written in Ludic — no C is generated, compiled or linked in a build.",
"og_title": "Ludic — a compiled game language",
"og_description": "Ahead-of-time compiled, with an ECS in the syntax and a deterministic fixed-point runtime. The compiler is written in Ludic; no C is generated, compiled or linked in a build."
},
"nav_links": [
{ "label": "Features", "href": "#features" },
{ "label": "Examples", "href": "#showcase" },
{ "label": "Get started", "href": "#start" },
{ "label": "API Reference", "href": "api.html" }
{
"label": "Features",
"href": "#features"
},
{
"label": "Examples",
"href": "#showcase"
},
{
"label": "Get started",
"href": "#start"
},
{
"label": "API Reference",
"href": "api.html"
}
],
"ref_nav_links": [
{ "label": "Home", "href": "index.html" },
{ "label": "API Reference", "href": "api.html" }
{
"label": "Home",
"href": "index.html"
},
{
"label": "API Reference",
"href": "api.html"
}
],
"hero": {
"pill": "Opinionated · Batteries-included · No framework",
"title_pre": "Build a game,",
"title_accent": "not a framework.",
"lead": "Ludic is an <b>opinionated, compiled language for 2D games</b>. The entity system, drawing, named colors, input, deterministic math and save/load aren't libraries you wire up — they're <b>part of the language</b>. You write the game; there's nothing to assemble first.",
"title_pre": "A compiled language",
"title_accent": "for 2D games.",
"lead": "Ludic compiles ahead of time to a native binary — no engine to install, no interpreter, nothing shipped beside the executable. The entity-component system is <b>part of the syntax</b>, the maths is <b>deterministic fixed-point</b>, and <code>ludicc</code> is itself written in Ludic: <b>no C is generated, compiled or linked in a build</b>.",
"snippet": "docs/site/snippets/hero.ludic",
"snippet_name": "hello.ludic",
"primary_cta": { "label": "Get started →", "href": "#start" },
"secondary_cta": { "label": "API Reference", "href": "api.html" },
"pipeline": [".ludic", "ludicc", "optimized IR", "native binary"],
"pipeline_note": "One step from your code to a native binary. No engine to install, no interpreter, no runtime to ship alongside it — the game <i>is</i> the executable."
"primary_cta": {
"label": "Get started →",
"href": "#start"
},
"secondary_cta": {
"label": "API Reference",
"href": "api.html"
},
"pipeline": [
".ludic",
"ludicc",
"LLVM IR",
"native binary"
],
"pipeline_note": "<code>ludicc</code> lowers straight to LLVM IR and links a native binary. There is no C step in between and no runtime to ship alongside — the game is the executable."
},
"features": {
"kicker": "Why Ludic",
"title": "A language shaped around the game.",
"intro": "The things you normally bolt on — an entity-component system, a renderer, a deterministic clock, save/load — are primitives of the language itself. One opinionated way to do each, so there's little to decide and nothing to wire up.",
"intro": "The things you normally bolt on — an entity-component system, a renderer, a deterministic clock, save/load — are primitives of the language itself. One way to do each, so there is little to decide and nothing to wire up.",
"cards": [
{ "icon": "🧩", "title": "ECS in the syntax", "html": "<code>property</code>, <code>model</code>, and <code>handler</code> are keywords. Query entities with <code>query [A, B, {Tag}]</code> and iterate matches directly — no framework to wire up." },
{ "icon": "⏱️", "title": "Deterministic runtime", "html": "Q16.16 <code>fixed</code>-point math and a seeded RNG mean the same inputs produce the same frame — byte-for-byte — every run. Ideal for replays and lockstep netcode." },
{ "icon": "🎨", "title": "Drawing &amp; named colors", "html": "The <code>Screen</code> API draws rectangles, text and pixels with named arguments; <code>Color.Crimson</code> and 220 more names read like English and cost nothing at runtime." },
{ "icon": "📦", "title": "One self-contained binary", "html": "A game compiles to a single native executable — no engine to install, no interpreter, no runtime shipped beside it. Build, and run the file." },
{ "icon": "💾", "title": "Snapshot save/load", "html": "<code>save()</code> and <code>load()</code> serialize the entire ECS world — every entity, property and program <code>var</code> — in one call." },
{ "icon": "🎬", "title": "Scenes &amp; state machines", "html": "<code>scene</code>/<code>layer</code>/<code>become</code> model mutually-exclusive game states with enter/exit hooks; <code>match</code>/<code>machine</code>/<code>state</code> handle dispatch and per-entity FSMs." },
{ "icon": "🖼️", "title": "Retained UI &amp; assets", "html": "Declare a widget tree as data with <code>ui</code> — panels, labels, buttons, 9-slice skins, keyboard focus. Sprites decode from PNG at runtime; text is real TrueType." },
{ "icon": "🔌", "title": "Modules &amp; native calls", "html": "<code>module</code> + <code>@export fn</code> builds a shared library of plain native symbols; <code>extern fn … = \"symbol\"</code> reaches out to any native library when you need the platform." }
{
"title": "ECS in the syntax",
"html": "<code>property</code>, <code>model</code>, and <code>handler</code> are keywords. Query entities with <code>query [A, B, {Tag}]</code> and iterate matches directly — no framework to wire up."
},
{
"title": "Deterministic runtime",
"html": "Q16.16 <code>fixed</code>-point math and a seeded RNG mean the same inputs produce the same frame — byte-for-byte — every run. Ideal for replays and lockstep netcode."
},
{
"title": "Drawing &amp; named colors",
"html": "The <code>Screen</code> API draws rectangles, text and pixels with named arguments; <code>Color.Crimson</code> and 220 more names read like English and cost nothing at runtime."
},
{
"title": "One self-contained binary",
"html": "A game compiles to a single native executable — no engine to install, no interpreter, no runtime shipped beside it. Build, and run the file."
},
{
"title": "Snapshot save/load",
"html": "<code>save()</code> and <code>load()</code> serialize the entire ECS world — every entity, property and program <code>var</code> — in one call."
},
{
"title": "Scenes &amp; state machines",
"html": "<code>scene</code>/<code>layer</code>/<code>become</code> model mutually-exclusive game states with enter/exit hooks; <code>match</code>/<code>machine</code>/<code>state</code> handle dispatch and per-entity FSMs."
},
{
"title": "Retained UI &amp; assets",
"html": "Declare a widget tree as data with <code>ui</code> — panels, labels, buttons, 9-slice skins, keyboard focus. Sprites decode from PNG at runtime; text is real TrueType."
},
{
"title": "Modules &amp; native calls",
"html": "<code>module</code> + <code>@export fn</code> builds a shared library of plain native symbols; <code>extern fn … = \"symbol\"</code> reaches out to any native library when you need the platform."
}
]
},
"showcase": {
"kicker": "Show, don't tell",
"title": "Real programs, one toolchain.",
"intro": "The same <code style=\"color:var(--mint)\">ludicc</code> that builds a JRPG builds a from-scratch Snake and a scene demo. Nothing is hardcoded to a genre — and every token below links into the reference.",
"intro": "The same <code>ludicc</code> that builds a JRPG builds a from-scratch Snake and a scene demo. Nothing is hardcoded to a genre — and every token below links into the reference.",
"samples": [
{ "label": "snake", "name": "snake.ludic", "file": "docs/site/snippets/snake.ludic",
"note": "A complete Snake — grid, growth, food, game-over — from primitives. State is named vars, colors are named, and every draw call says what each argument is." },
{ "label": "scenes", "name": "scenes.ludic", "file": "docs/site/snippets/scenes.ludic",
"note": "One active scene at a time. `become` runs the old scene's on-exit and the new one's on-enter; layers draw in declaration order. State is a plain named var." },
{ "label": "lifecycle", "name": "toggle.ludic", "file": "docs/site/snippets/lifecycle.ludic",
"note": "Enable/disable at three scopes — entity, model, handler. Disabling never destroys data: a property's values persist, so a later enable restores them." },
{ "label": "hello", "name": "hello.ludic", "file": "docs/site/snippets/hero.ludic",
"note": "The smallest program that exercises the whole pipeline: properties, a spawn, a queried handler, and a render/quit." }
{
"label": "snake",
"name": "snake.ludic",
"file": "docs/site/snippets/snake.ludic",
"note": "A complete Snake — grid, growth, food, game-over — from primitives. State is named vars, colors are named, and every draw call says what each argument is."
},
{
"label": "scenes",
"name": "scenes.ludic",
"file": "docs/site/snippets/scenes.ludic",
"note": "One active scene at a time. `become` runs the old scene's on-exit and the new one's on-enter; layers draw in declaration order. State is a plain named var."
},
{
"label": "lifecycle",
"name": "toggle.ludic",
"file": "docs/site/snippets/lifecycle.ludic",
"note": "Enable/disable at three scopes — entity, model, handler. Disabling never destroys data: a property's values persist, so a later enable restores them."
},
{
"label": "hello",
"name": "hello.ludic",
"file": "docs/site/snippets/hero.ludic",
"note": "The smallest program that exercises the whole pipeline: properties, a spawn, a queried handler, and a render/quit."
}
]
},
"philosophy": {
"kicker": "The philosophy",
"title": "No framework. No glue. Just the game.",
"title": "Batteries in the language, not in a framework.",
"paras": [
"Most game code is plumbing — registering systems, wiring a renderer, threading state through a framework. Ludic makes those decisions for you and bakes them into the language, so the code you write is the game's actual logic.",
"Opinionated on purpose: one clear way to spawn an entity, draw a frame, name a color, run a scene. Less to choose, less to learn, less to maintain."
"Most game code is plumbing — registering systems, wiring a renderer, threading state through a framework. Ludic makes those decisions once and bakes them into the language, so the code you write is the game's actual logic.",
"That extends to the toolchain. <code>ludicc</code> is written in Ludic and recompiles its own source to a byte-identical binary, rebuilding from a checked-in IR seed with clang and nothing else."
],
"stats": [
{ "big": "0", "lbl": "engines to install" },
{ "big": "1", "lbl": "clear way to do each thing" },
{ "big": "=", "lbl": "deterministic frames" }
{
"big": "0",
"lbl": "lines of C in a build"
},
{
"big": "1",
"lbl": "backend: LLVM IR"
},
{
"big": "=",
"lbl": "byte-exact self-rebuild"
}
]
},
"start": {
"kicker": "Get started",
"title": "From clone to a native window.",
"intro": "Build the toolchain once, then your game. Everything runs from the repo root.",
"intro": "Bootstrap the task runner once from the checked-in IR seed, then build the toolchain and your game. Everything runs from the repo root.",
"steps": [
{ "title": "Build the toolchain", "html": "<code>bin/x build</code> produces <code>ludicc</code>, the compiler you'll use for everything below." },
{ "title": "Compile &amp; run an example", "html": "<code>bin/x app examples/games/snake.ludic</code> turns a <code>.ludic</code> file into a native binary. Run it to open a real window." },
{ "title": "Go headless for tests", "html": "<code>--headless</code> renders frames to a <code>.ppm</code> from piped input — deterministic output you can diff." },
{ "title": "Edit with full tooling", "html": "<code>bin/x tools</code> builds the formatter and language server; every editor gets completion, diagnostics and go-to-definition." }
{
"title": "Bootstrap from the seed",
"html": "<code>bin/</code> is not checked in, so it is created first; then <code>clang</code> assembles the compiler's own checked-in LLVM IR seed, and that compiler builds <code>bin/x</code>, the task runner. This is the only step Ludic cannot do for itself."
},
{
"title": "Build the toolchain",
"html": "<code>bin/x build</code> produces <code>ludicc</code>, <code>ludic</code>, <code>ludic-fmt</code> and <code>ludic-lsp</code> — all compiled by Ludic, from Ludic."
},
{
"title": "Compile &amp; run an example",
"html": "<code>bin/x app examples/games/snake.ludic</code> turns a <code>.ludic</code> file into a native binary. Run it to open a real window."
},
{
"title": "Go headless for tests",
"html": "<code>--headless</code> renders frames to a <code>.ppm</code> from piped input — deterministic output you can diff in CI."
}
],
"terminal_name": "zsh — ludic",
"terminal": [
{ "comment": "build the toolchain (once)" },
{ "cmd": "bin/x build" },
{ "blank": true },
{ "comment": "build and run an example (opens a window)" },
{ "cmd": "bin/x app examples/games/snake.ludic" },
{ "cmd": "./build/snake" },
{ "blank": true },
{ "comment": "deterministic headless render for tests" },
{ "cmd": "bin/x app examples/games/snake.ludic --headless" },
{ "cmd": "printf 'ddddwww' | ./build/snake_headless" },
{ "out": "→ writes out.ppm" }
{
"comment": "bootstrap the task runner (clang + the IR seed, once)"
},
{
"cmd": "mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc"
},
{
"cmd": "bin/ludicc tools/x/main.ludic -o bin/x"
},
{
"blank": true
},
{
"comment": "build the toolchain, then an example (opens a window)"
},
{
"cmd": "bin/x build"
},
{
"cmd": "bin/x app examples/games/snake.ludic"
},
{
"cmd": "./build/snake"
},
{
"blank": true
},
{
"comment": "deterministic headless render for tests"
},
{
"cmd": "bin/x app examples/games/snake.ludic --headless"
},
{
"cmd": "printf 'ddddwww' | ./build/snake_headless"
},
{
"out": "→ writes build/out.ppm"
}
]
},
"editors": {
"kicker": "Editor experience",
"title": "One language server, every editor.",
"intro": "<code>ludic-lsp</code> speaks LSP 3.17 over stdio: context-aware completion, diagnostics from the compiler itself, go-to-definition and rename across <code>import</code>ed files, and comment-preserving formatting. It even understands <code>```ludic</code> fences in Markdown.",
"list": ["VS Code", "JetBrains IDEs", "Neovim", "Helix", "Emacs", "Sublime Text", "Zed"],
"list": [
"VS Code",
"JetBrains IDEs",
"Neovim",
"Helix",
"Emacs",
"Sublime Text",
"Zed"
],
"note": "<code>bin/x tools</code> builds <span class=\"mono\">ludic-fmt</span> and <span class=\"mono\">ludic-lsp</span> — the same formatter runs as a CLI for pre-commit hooks and CI."
}
}

View file

@ -9,7 +9,7 @@ program Hello {
spawn Enemy { Position { column: 10, row: 2 }, Velocity { delta_x: 0, delta_y: 1 } }
}
# a handler declares the models it touches; the body
# a handler declares the entities it touches; the body
# runs once per match, each property bound by name.
@Queries(these: [Position, Velocity])
handler AdvancePositions phase FixedUpdate {

View file

@ -1,83 +0,0 @@
:root{
--bg:#0a0d18; --bg2:#0d1122; --panel:#12172b; --panel2:#151b32;
--line:#232b47; --line2:#2c3660; --text:#c9d2ea; --head:#f2f5ff; --muted:#8290b4;
--mint:#7cf5c4; --coral:#ff5d73; --violet:#c792ea; --blue:#82aaff; --amber:#f6c177;
--c-bg:#0b0f20; --c-comment:#5b6788; --c-key:#ff7eb6; --c-type:#7cf5c4;
--c-str:#f6c177; --c-num:#a6b8ff; --c-annot:#c792ea; --c-fn:#82aaff; --c-punc:#9aa6cc;
--radius:14px; --max:1180px;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;background:var(--bg);color:var(--text);
font-family:"Space Grotesk",system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.6;
-webkit-font-smoothing:antialiased}
a{color:inherit;text-decoration:none}
code,pre,.mono{font-family:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace}
.wrap{max-width:var(--max);margin:0 auto;padding:0 24px}
header.nav{position:sticky;top:0;z-index:50;backdrop-filter:blur(10px);
background:rgba(10,13,24,.78);border-bottom:1px solid var(--line)}
.nav-in{display:flex;align-items:center;gap:22px;height:64px}
.brand{display:flex;align-items:center;gap:10px;font-weight:700;color:var(--head);font-size:18px}
.logo{width:26px;height:26px;border-radius:7px;display:grid;place-items:center;
background:linear-gradient(135deg,var(--mint),#3fd7a6);color:#06231a;font-weight:700;font-size:15px}
.nav-links{display:flex;gap:24px;margin-left:auto;align-items:center}
.nav-links a{color:var(--muted);font-size:14.5px;font-weight:500;transition:color .15s}
.nav-links a:hover{color:var(--head)}
.nav-cta{border:1px solid var(--line2);padding:8px 15px;border-radius:9px;color:var(--head)!important;background:var(--panel)}
.nav-cta:hover{border-color:var(--mint)}
@media(max-width:760px){.nav-links a:not(.nav-cta){display:none}}
/* two-column reference layout */
.ref-layout{display:grid;grid-template-columns:230px 1fr;gap:40px;align-items:start;padding-top:34px;padding-bottom:80px}
@media(max-width:900px){.ref-layout{grid-template-columns:1fr}.side{display:none}}
.side{position:sticky;top:88px;display:flex;flex-direction:column;gap:2px;max-height:calc(100vh - 110px);overflow:auto}
.side a{color:var(--muted);font-size:13.5px;padding:6px 10px;border-radius:8px;border-left:2px solid transparent;transition:all .12s}
.side a:hover{color:var(--head);background:var(--panel)}
.side a.active{color:var(--mint);border-left-color:var(--mint);background:rgba(124,245,196,.06)}
.ref-intro{margin-bottom:44px}
.ref-intro .kicker{font-size:13px;font-weight:600;letter-spacing:2px;text-transform:uppercase;color:var(--mint);margin-bottom:12px}
.ref-intro h1{font-size:clamp(30px,5vw,46px);margin:0 0 12px;color:var(--head);letter-spacing:-1px;font-weight:700}
.ref-intro p{font-size:17px;color:var(--muted);max-width:640px;margin:0}
.ref-sec{padding:26px 0 10px;border-top:1px solid var(--line);margin-top:26px}
.ref-sec:first-of-type{border-top:none;margin-top:0}
.ref-sec h2{font-size:24px;color:var(--head);margin:0 0 6px;letter-spacing:-.4px}
.sec-blurb{color:var(--muted);font-size:15px;margin:0 0 22px;max-width:720px}
.sec-blurb code{color:var(--mint);background:rgba(124,245,196,.08);padding:1px 5px;border-radius:5px;font-size:12.5px}
.entry{padding:16px 0;border-top:1px dashed var(--line)}
.entry:first-of-type{border-top:none}
.entry-head{display:flex;align-items:center;gap:10px}
.entry h3{margin:0;font-size:16.5px;color:var(--head);font-weight:600;font-family:"JetBrains Mono",monospace}
.entry .anchor{color:var(--line2);font-size:15px;opacity:0;transition:opacity .12s}
.entry:hover .anchor{opacity:1}
.entry .anchor:hover{color:var(--mint)}
.entry .sig{display:inline-block;margin:8px 0 6px;color:var(--c-fn);background:var(--c-bg);
border:1px solid var(--line);border-radius:8px;padding:5px 10px;font-size:13px}
.entry p{margin:6px 0 0;color:var(--text);font-size:14.5px;max-width:720px}
.entry p code{color:var(--amber);background:var(--panel);padding:1px 5px;border-radius:5px;font-size:12.5px}
.entry p b{color:var(--head)}
pre{margin:10px 0 0;padding:14px 16px;overflow:auto;font-size:13px;line-height:1.7;tab-size:2;
background:var(--c-bg);border:1px solid var(--line);border-radius:10px}
pre.ex{max-width:720px}
pre::-webkit-scrollbar{height:8px}
pre::-webkit-scrollbar-thumb{background:var(--line2);border-radius:6px}
.t-com{color:var(--c-comment);font-style:italic}
.t-key{color:var(--c-key)}.t-type{color:var(--c-type)}.t-str{color:var(--c-str)}
.t-num{color:var(--c-num)}.t-annot{color:var(--c-annot)}.t-fn{color:var(--c-fn)}.t-punc{color:var(--c-punc)}
/* hover-jump links inside code */
a.tok{border-radius:3px;transition:background .12s}
a.tok:hover{background:rgba(124,245,196,.14);outline:1px solid rgba(124,245,196,.35)}
/* color swatches */
.swatch-group{margin-bottom:22px}
.swatch-group h4{margin:0 0 12px;font-size:13px;letter-spacing:1.5px;text-transform:uppercase;color:var(--muted);font-weight:600}
.swatch-row{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px}
.swatch{display:flex;align-items:center;gap:10px;background:var(--panel);border:1px solid var(--line);
border-radius:9px;padding:8px 10px}
.chip{width:24px;height:24px;border-radius:6px;flex:none;box-shadow:inset 0 0 0 1px rgba(255,255,255,.12)}
.cname{font-family:"JetBrains Mono",monospace;font-size:12px;color:var(--head);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.chex{margin-left:auto;font-family:"JetBrains Mono",monospace;font-size:11px;color:var(--muted)}

View file

@ -0,0 +1,268 @@
/* base.css — the design tokens and shared chrome for every generated page.
*
* The site is deliberately typographic and light-first: a warm paper ground, a
* serif display face for headings, one ink-blue accent used only where it means
* something (links, the current page, a focused control), and colour otherwise
* reserved for code. Dark mode is the same design with the ground inverted, not
* a second theme — every colour below is a token, redefined once under
* prefers-color-scheme and nowhere else.
*
* Fonts are the platform's own. A language reference should render instantly
* and identically offline; a webfont round-trip buys nothing here. */
:root {
/* ground and ink */
--paper: #fcfcfa;
--paper-2: #f5f4f0;
--panel: #ffffff;
--ink: #17171a;
--ink-2: #3d3d44;
--muted: #67676f;
--rule: #e3e2dc;
--rule-2: #cfcec6;
/* the single accent */
--accent: #0f4c81;
--accent-ink: #0b3a63;
--accent-bg: #eaf1f8;
/* code surface and tokens */
--code-bg: #f7f6f2;
--code-rule: #e3e2dc;
--c-comment: #7a7a70;
--c-key: #8f2d56;
--c-type: #0f6b5c;
--c-str: #9a5518;
--c-num: #1d4f8c;
--c-annot: #6b3fa0;
--c-fn: #0f4c81;
--c-punc: #6a6a72;
--c-arg: #9a5518;
--radius: 6px;
--max: 1080px;
--hdr: 58px;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--serif: ui-serif, "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}
@media (prefers-color-scheme: dark) {
:root {
--paper: #16161a;
--paper-2: #1c1c21;
--panel: #1a1a1f;
--ink: #e6e5e0;
--ink-2: #c2c1bb;
--muted: #8f8f98;
--rule: #2b2b32;
--rule-2: #3d3d46;
--accent: #7fb0dd;
--accent-ink: #a8ccec;
--accent-bg: #1b2733;
--code-bg: #1b1b21;
--code-rule: #2b2b32;
--c-comment: #7b7b84;
--c-key: #e08aa8;
--c-type: #64c9b4;
--c-str: #d9a066;
--c-num: #8fb8e8;
--c-annot: #b596e0;
--c-fn: #7fb0dd;
--c-punc: #8b8b94;
--c-arg: #d9a066;
}
}
*, *::before, *::after { box-sizing: border-box }
html {
-webkit-text-size-adjust: 100%;
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto }
}
body {
margin: 0;
background: var(--paper);
color: var(--ink-2);
font-family: var(--sans);
font-size: 16px;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
h1, h2, h3, h4 {
font-family: var(--serif);
color: var(--ink);
font-weight: 600;
line-height: 1.2;
letter-spacing: -0.005em;
margin: 0;
}
a { color: var(--accent); text-decoration: none }
a:hover { text-decoration: underline; text-underline-offset: 2px }
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 3px;
}
.wrap { max-width: var(--max); margin: 0 auto; padding: 0 24px }
.muted { color: var(--muted) }
.mono, code, pre, kbd { font-family: var(--mono) }
/* Inline code carries no chip: the mono face plus the darker ink is already a
clear enough break from running text, and a tinted box behind every keyword
in a language reference turns a paragraph into confetti. */
code {
font-size: 0.905em;
color: var(--ink);
}
pre code { font-size: inherit }
pre {
margin: 14px 0 0;
padding: 15px 17px;
overflow-x: auto;
font-size: 13px;
line-height: 1.7;
tab-size: 2;
background: var(--code-bg);
border: 1px solid var(--code-rule);
border-radius: var(--radius);
color: var(--ink);
}
pre::-webkit-scrollbar { height: 9px }
pre::-webkit-scrollbar-thumb { background: var(--rule-2); border-radius: 5px }
/* syntax tokens, shared by the highlighter and the hand-written terminal block */
.t-com { color: var(--c-comment); font-style: italic }
.t-key { color: var(--c-key) }
.t-type { color: var(--c-type) }
.t-str { color: var(--c-str) }
.t-num { color: var(--c-num) }
.t-annot { color: var(--c-annot) }
.t-fn { color: var(--c-fn) }
.t-punc { color: var(--c-punc) }
.t-arg { color: var(--c-arg) }
/* ---------- header ---------- */
header.nav {
position: sticky;
top: 0;
z-index: 50;
background: color-mix(in srgb, var(--paper) 92%, transparent);
backdrop-filter: saturate(1.4) blur(8px);
border-bottom: 1px solid var(--rule);
}
.nav-in { display: flex; align-items: center; gap: 14px; min-height: var(--hdr) }
.brand {
display: inline-flex;
align-items: center;
gap: 9px;
font-family: var(--serif);
font-weight: 600;
font-size: 18px;
color: var(--ink);
letter-spacing: -0.01em;
}
.brand:hover { text-decoration: none }
.logo {
width: 23px; height: 23px;
flex: none;
display: grid;
place-items: center;
border: 1px solid var(--rule-2);
border-radius: 4px;
background: var(--panel);
font-family: var(--mono);
font-size: 12px;
font-weight: 500;
color: var(--accent);
}
.nav-toggle {
margin-left: auto;
display: inline-flex;
align-items: center;
justify-content: center;
width: 38px; height: 38px;
border: 1px solid var(--rule-2);
border-radius: var(--radius);
background: var(--panel);
color: var(--ink);
font-size: 17px;
line-height: 1;
cursor: pointer;
}
.nav-links {
display: none;
position: absolute;
top: 100%; left: 0; right: 0;
flex-direction: column;
background: var(--paper);
border-bottom: 1px solid var(--rule);
padding: 0 24px 8px;
}
header.nav.open .nav-links { display: flex }
.nav-links a {
color: var(--ink-2);
font-size: 15px;
padding: 13px 2px;
border-top: 1px solid var(--rule);
}
.nav-links a:first-child { border-top: none }
@media (min-width: 760px) {
.nav-toggle { display: none }
.nav-links {
display: flex;
position: static;
flex-direction: row;
align-items: center;
gap: 26px;
margin-left: auto;
padding: 0;
border: none;
background: none;
}
.nav-links a {
color: var(--muted);
font-size: 14.5px;
padding: 0;
border: none;
}
.nav-links a:hover { color: var(--ink); text-decoration: none }
.nav-links .nav-cta { color: var(--ink) }
}
/* ---------- footer ---------- */
footer {
margin-top: 84px;
border-top: 1px solid var(--rule);
padding: 32px 0 44px;
}
.foot-in {
display: flex;
flex-wrap: wrap;
gap: 22px;
align-items: flex-start;
justify-content: space-between;
}
.foot-in .muted { font-size: 14px; max-width: 42ch }
.foot-links { display: flex; flex-wrap: wrap; gap: 20px }
.foot-links a { color: var(--muted); font-size: 14px }
.foot-links a:hover { color: var(--ink) }

View file

@ -0,0 +1,249 @@
/* docs.css — every reference page: a symbol page, a namespace overview, the
* color palette, and the API index. Loaded after base.css, which owns the
* tokens. (This replaces the old item.css/api.css pair; api.css was never
* referenced by the generator at all.)
*
* A reference page is a document, so it is set as one: a measured column, a
* serif heading, a signature that reads as code, and rules instead of cards. */
main { padding: 34px 0 0 }
/* ---------- breadcrumbs ---------- */
.crumbs {
font-size: 13px;
color: var(--muted);
display: flex;
flex-wrap: wrap;
gap: 7px;
margin-bottom: 20px;
}
.crumbs a { color: var(--muted) }
.crumbs a:hover { color: var(--ink) }
.crumbs .here { color: var(--ink) }
/* ---------- symbol head ---------- */
.item { max-width: 78ch }
.item-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap }
.item-head h1 {
font-size: clamp(27px, 4vw, 36px);
letter-spacing: -0.02em;
word-break: break-word;
}
.kind-badge {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
border: 1px solid var(--rule-2);
border-radius: 3px;
padding: 2px 7px;
white-space: nowrap;
}
/* the accent is the only colour that distinguishes a kind, and only for the
two that are structural rather than callable */
.kind-namespace, .kind-keyword { color: var(--accent); border-color: var(--accent) }
.sig {
display: block;
margin: 18px 0 0;
padding: 11px 14px;
font-size: 14px;
background: var(--code-bg);
border: 1px solid var(--code-rule);
border-left: 2px solid var(--rule-2);
border-radius: var(--radius);
color: var(--ink);
overflow-x: auto;
white-space: pre;
}
.desc { margin-top: 20px }
.desc p { margin: 0 0 14px }
.desc p:last-child { margin-bottom: 0 }
.ns-blurb { margin: 20px 0 0; color: var(--ink-2) }
/* ---------- sections within a page ---------- */
.params h2, .examples h2, .related h2, .swatch-group h3 {
font-family: var(--sans);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
margin: 34px 0 12px;
padding-bottom: 7px;
border-bottom: 1px solid var(--rule);
}
.param {
display: grid;
gap: 3px 14px;
padding: 9px 0;
border-bottom: 1px solid var(--rule);
}
@media (min-width: 620px) {
.param { grid-template-columns: minmax(90px, 22%) 1fr; align-items: baseline }
}
.param:last-child { border-bottom: none }
.pname { color: var(--c-arg); font-size: 13.5px }
.pdesc { color: var(--ink-2); font-size: 15px }
.examples pre { margin-top: 0 }
.examples pre + pre { margin-top: 12px }
.rel-row { display: flex; flex-wrap: wrap; gap: 8px }
.rel {
font-family: var(--mono);
font-size: 13px;
color: var(--ink-2);
border: 1px solid var(--rule-2);
border-radius: var(--radius);
padding: 4px 10px;
}
.rel:hover { color: var(--accent); border-color: var(--accent); text-decoration: none }
.back {
display: inline-block;
margin: 40px 0 0;
font-size: 14px;
color: var(--muted);
}
.back:hover { color: var(--ink) }
/* ---------- namespace overview ---------- */
.ns-methods { margin-top: 26px; border-top: 1px solid var(--rule) }
.ns-method {
display: grid;
gap: 2px;
padding: 12px 2px;
border-bottom: 1px solid var(--rule);
color: inherit;
}
@media (min-width: 720px) {
.ns-method { grid-template-columns: minmax(0, 46%) 1fr; gap: 18px; align-items: baseline }
}
.ns-method:hover { background: var(--paper-2); text-decoration: none }
.nm-sig { font-size: 13.5px; color: var(--c-fn) }
.nm-tip { color: var(--muted); font-size: 14.5px }
/* ---------- color palette ---------- */
.swatch-row {
display: grid;
border-top: 1px solid var(--rule);
border-left: 1px solid var(--rule);
border-radius: var(--radius);
overflow: hidden;
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
}
.swatch {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 11px;
font-size: 13px;
border-right: 1px solid var(--rule);
border-bottom: 1px solid var(--rule);
}
.swatch.flash { background: var(--accent-bg) }
.chip {
width: 17px; height: 17px;
flex: none;
border-radius: 3px;
border: 1px solid rgba(0, 0, 0, .18);
}
.cname { font-family: var(--mono); color: var(--ink); overflow: hidden; text-overflow: ellipsis }
.chex { font-family: var(--mono); color: var(--muted); margin-left: auto; font-size: 12px }
/* ---------- API index ---------- */
.ref { max-width: var(--max) }
.ref-intro { max-width: 68ch; margin-bottom: 40px }
.ref-intro h1 { font-size: clamp(28px, 4vw, 38px); letter-spacing: -0.02em; margin: 0 0 12px }
.ref-intro p { color: var(--muted); margin: 0 0 22px }
.search {
width: 100%;
font-family: var(--sans);
font-size: 15px;
color: var(--ink);
background: var(--panel);
border: 1px solid var(--rule-2);
border-radius: var(--radius);
padding: 11px 14px;
}
.search::placeholder { color: var(--muted) }
.search:focus { outline: none; border-color: var(--accent) }
.noresults { margin-top: 14px; color: var(--muted); font-size: 14.5px }
.idx-sec { margin-bottom: 44px }
.idx-sec h2 {
font-size: 20px;
margin: 0 0 6px;
padding-bottom: 9px;
border-bottom: 1px solid var(--rule);
}
.sec-blurb { color: var(--muted); font-size: 14.5px; margin: 0 0 16px; max-width: 74ch }
.idx-grid {
display: grid;
border-top: 1px solid var(--rule);
border-left: 1px solid var(--rule);
border-radius: var(--radius);
overflow: hidden;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.idx-item {
display: grid;
gap: 1px;
padding: 11px 14px;
color: inherit;
border-right: 1px solid var(--rule);
border-bottom: 1px solid var(--rule);
}
.idx-item:hover { background: var(--paper-2); text-decoration: none }
.idx-item code { font-size: 13.5px; color: var(--c-fn) }
.idx-item span { color: var(--muted); font-size: 13.5px }
/* ---------- hover cards + deep-link flash (from ludic-highlight.js) ------- */
.tok { cursor: help; border-bottom: 1px dotted var(--rule-2) }
a.tok { cursor: pointer }
.hovercard {
position: absolute;
z-index: 90;
max-width: 360px;
background: var(--panel);
border: 1px solid var(--rule-2);
border-radius: var(--radius);
box-shadow: 0 8px 24px -12px rgba(0, 0, 0, .35);
padding: 11px 13px;
font-size: 13.5px;
}
.hc-top { display: flex; align-items: center; gap: 9px; margin-bottom: 6px }
.hc-name { font-family: var(--mono); font-size: 13.5px; color: var(--ink) }
.hc-sig { font-family: var(--mono); font-size: 12.5px; color: var(--muted); display: block; margin-bottom: 6px }
.hc-tip { color: var(--ink-2) }
.hc-foot { display: block; margin-top: 8px; font-size: 12px; color: var(--muted) }
:target { scroll-margin-top: calc(var(--hdr) + 16px) }
.flash { animation: flash 1.1s ease-out }
@keyframes flash {
from { background: var(--accent-bg) }
to { background: transparent }
}
/* No standing tint here: `animation: none` plus a background would leave the
highlight on the element forever, which is exactly what a reader who asked
for less motion did not ask for. */
@media (prefers-reduced-motion: reduce) {
.flash { animation: none }
}

View file

@ -8,12 +8,8 @@
<meta property="og:title" content="%(ogt)s">
<meta property="og:description" content="%(ogd)s">
<meta property="og:type" content="website">
<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=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
%(css)s
</style>
<link rel="stylesheet" href="base.css">
<link rel="stylesheet" href="site.css">
</head>
<body>
<header class="nav">
@ -30,7 +26,6 @@
<section class="hero">
<div class="wrap hero-grid">
<div>
<span class="pill"><span class="dot"></span> %(pill)s</span>
<h1>%(title_pre)s <span class="accent">%(title_accent)s</span></h1>
<p class="lead">%(lead)s</p>
<div class="cta-row">
@ -38,23 +33,20 @@
<a class="btn btn-ghost" href="%(scta_h)s">%(scta_l)s</a>
</div>
</div>
<div class="code-card reveal">
<div class="code-top">
<span class="tl r"></span><span class="tl y"></span><span class="tl g"></span>
<span class="code-name">%(hero_name)s</span>
</div>
<div class="code-card">
<div class="code-top"><span class="code-name">%(hero_name)s</span></div>
<pre data-lang="ludic">%(hero_code)s</pre>
</div>
</div>
<div class="wrap" style="margin-top:52px">
<div class="pipeline reveal">%(stages)s</div>
<p style="text-align:center;color:var(--muted);font-size:13.5px;margin:14px 0 0">%(pipenote)s</p>
<div class="wrap">
<div class="pipeline">%(stages)s</div>
<p class="pipe-note">%(pipenote)s</p>
</div>
</section>
<section id="features">
<div class="wrap">
<div class="sec-head reveal">
<div class="sec-head">
<div class="kicker">%(feat_kicker)s</div>
<h2>%(feat_title)s</h2>
<p>%(feat_intro)s</p>
@ -63,24 +55,24 @@
</div>
</section>
<section id="showcase" style="padding-top:24px">
<section id="showcase">
<div class="wrap">
<div class="sec-head reveal">
<div class="sec-head">
<div class="kicker">%(sc_kicker)s</div>
<h2>%(sc_title)s</h2>
<p>%(sc_intro)s</p>
</div>
<div class="tabs reveal" id="tabs"></div>
<div class="tabs" id="tabs" role="tablist"></div>
<div id="panels"></div>
<div class="showcase-note reveal"><span class="b">↳</span><span id="note"></span></div>
<div class="showcase-note"><span class="b">↳</span><span id="note"></span></div>
</div>
</section>
<section style="padding-top:24px">
<section id="philosophy">
<div class="wrap">
<div class="banner reveal">
<div class="banner">
<div>
<div class="kicker" style="color:var(--blue)">%(phil_kicker)s</div>
<div class="kicker">%(phil_kicker)s</div>
<h2>%(phil_title)s</h2>
%(phil_paras)s
</div>
@ -91,40 +83,37 @@
<section id="start">
<div class="wrap">
<div class="sec-head reveal">
<div class="sec-head">
<div class="kicker">%(start_kicker)s</div>
<h2>%(start_title)s</h2>
<p>%(start_intro)s</p>
</div>
<div class="steps">
<div>%(steps)s</div>
<div class="term reveal">
<div class="code-top">
<span class="tl r"></span><span class="tl y"></span><span class="tl g"></span>
<span class="code-name">%(term_name)s</span>
</div>
<div class="term">
<div class="code-top"><span class="code-name">%(term_name)s</span></div>
<pre>%(term)s</pre>
</div>
</div>
</div>
</section>
<section id="editors" style="padding-top:24px">
<section id="editors">
<div class="wrap">
<div class="sec-head reveal">
<div class="sec-head">
<div class="kicker">%(ed_kicker)s</div>
<h2>%(ed_title)s</h2>
<p>%(ed_intro)s</p>
</div>
<div class="editors reveal">%(eds)s</div>
<p style="margin-top:22px;color:var(--muted);font-size:14.5px">%(ed_note)s</p>
<div class="editors">%(eds)s</div>
<p class="ed-note">%(ed_note)s</p>
</div>
</section>
<footer>
<div class="wrap foot-in">
<div>
<div class="brand" style="margin-bottom:8px"><span class="logo">L</span> %(brand)s</div>
<div class="brand" style="margin-bottom:6px"><span class="logo">L</span> %(brand)s</div>
<div class="muted">%(tagline)s</div>
</div>
<nav class="foot-links">%(footlinks)s</nav>
@ -141,27 +130,31 @@ const noteEl = document.getElementById("note");
SAMPLES.forEach((s, idx) => {
const t = document.createElement("button");
t.className = "tab" + (idx === 0 ? " active" : "");
t.type = "button";
t.setAttribute("role", "tab");
t.setAttribute("aria-selected", idx === 0 ? "true" : "false");
t.textContent = s.label;
t.onclick = () => select(idx);
tabsEl.appendChild(t);
const card = document.createElement("div");
card.className = "code-card panel-code" + (idx === 0 ? " active" : "");
card.innerHTML =
'<div class="code-top"><span class="tl r"></span><span class="tl y"></span><span class="tl g"></span><span class="code-name">'
+ s.name + '</span></div><pre>' + Ludic.highlight(s.code) + '</pre>';
'<div class="code-top"><span class="code-name"></span></div><pre></pre>';
card.querySelector(".code-name").textContent = s.name;
card.querySelector("pre").innerHTML = Ludic.highlight(s.code);
panelsEl.appendChild(card);
});
function select(idx) {
[...tabsEl.children].forEach((t,i)=>t.classList.toggle("active", i===idx));
[...tabsEl.children].forEach((t, i) => {
const on = i === idx;
t.classList.toggle("active", on);
t.setAttribute("aria-selected", on ? "true" : "false");
});
[...panelsEl.children].forEach((p, i) => p.classList.toggle("active", i === idx));
noteEl.textContent = SAMPLES[idx].note;
}
noteEl.textContent = SAMPLES[0].note;
Ludic.installCards();
const io = new IntersectionObserver((entries)=>{
entries.forEach(e=>{ if(e.isIntersecting){ e.target.classList.add("in"); io.unobserve(e.target); } });
},{threshold:.12});
document.querySelectorAll(".reveal").forEach(el=>io.observe(el));
</script>
</body>
</html>

View file

@ -1,176 +0,0 @@
:root{
--bg:#0a0d18; --bg2:#0d1122; --panel:#12172b; --panel2:#151b32;
--line:#232b47; --line2:#2c3660; --text:#c9d2ea; --head:#f2f5ff; --muted:#8290b4;
--mint:#7cf5c4; --coral:#ff5d73; --violet:#c792ea; --blue:#82aaff; --amber:#f6c177;
--c-bg:#0b0f20; --c-comment:#5b6788; --c-key:#ff7eb6; --c-type:#7cf5c4;
--c-str:#f6c177; --c-num:#a6b8ff; --c-annot:#c792ea; --c-fn:#82aaff; --c-punc:#9aa6cc; --c-arg:#f6c177;
--radius:14px; --max:1120px; --hdr:60px;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
body{margin:0;overflow-x:hidden;background:
radial-gradient(900px 500px at 82% -8%, rgba(124,245,196,.07), transparent 60%),
var(--bg);color:var(--text);
font-family:"Space Grotesk",system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.6;-webkit-font-smoothing:antialiased}
a{color:inherit;text-decoration:none}
code,pre,.mono{font-family:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace}
.wrap{max-width:var(--max);margin:0 auto;padding:0 16px}
/* ---- nav (mobile-first: hamburger) ---- */
header.nav{position:sticky;top:0;z-index:50;backdrop-filter:blur(10px);
background:rgba(10,13,24,.92);border-bottom:1px solid var(--line)}
.nav-in{display:flex;align-items:center;gap:12px;min-height:var(--hdr)}
.brand{display:flex;align-items:center;gap:9px;font-weight:700;color:var(--head);font-size:17px}
.logo{width:26px;height:26px;border-radius:7px;display:grid;place-items:center;flex:none;
background:linear-gradient(135deg,var(--mint),#3fd7a6);color:#06231a;font-weight:700;font-size:15px}
.nav-toggle{margin-left:auto;display:inline-flex;align-items:center;justify-content:center;
width:42px;height:42px;border-radius:10px;border:1px solid var(--line2);background:var(--panel);
color:var(--head);font-size:20px;cursor:pointer;line-height:1}
.nav-links{display:none;position:absolute;top:100%;left:0;right:0;flex-direction:column;
background:rgba(10,13,24,.98);border-bottom:1px solid var(--line);padding:4px 16px 10px;
box-shadow:0 20px 40px -24px rgba(0,0,0,.8)}
header.nav.open .nav-links{display:flex}
.nav-links a{color:var(--text);font-size:16px;font-weight:500;padding:14px 4px;border-top:1px solid var(--line)}
.nav-links a:first-child{border-top:none}
.nav-links a:active{color:var(--mint)}
.nav-links .nav-cta{background:none;text-align:left;color:var(--text)!important}
/* code token colors + snippet links */
pre{margin:12px 0 0;padding:14px;overflow:auto;-webkit-overflow-scrolling:touch;font-size:12.5px;line-height:1.7;tab-size:2;
max-width:100%;background:var(--c-bg);border:1px solid var(--line);border-radius:12px}
pre::-webkit-scrollbar{height:8px}
pre::-webkit-scrollbar-thumb{background:var(--line2);border-radius:6px}
.t-com{color:var(--c-comment);font-style:italic}
.t-key{color:var(--c-key)}.t-type{color:var(--c-type)}.t-str{color:var(--c-str)}
.t-num{color:var(--c-num)}.t-annot{color:var(--c-annot)}.t-fn{color:var(--c-fn)}
.t-punc{color:var(--c-punc)}.t-arg{color:var(--c-arg)}
a.tok{border-radius:3px;transition:background .12s,outline-color .12s}
a.tok:hover{background:rgba(124,245,196,.15);outline:1px solid rgba(124,245,196,.4)}
/* ---- item page ---- */
main.item{padding:22px 16px 72px;max-width:var(--max);margin:0 auto}
.crumbs{font-size:13px;color:var(--muted);margin-bottom:16px;line-height:1.7}
.crumbs a:active,.crumbs a:hover{color:var(--mint)}
.crumbs span{opacity:.6;margin:0 2px}
.crumbs .here{color:var(--text);opacity:1}
.item-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;scroll-margin-top:calc(var(--hdr) + 16px)}
.item-head h1{margin:0;font-size:clamp(24px,7vw,38px);color:var(--head);letter-spacing:-.5px;
font-family:"JetBrains Mono",monospace;font-weight:700;overflow-wrap:anywhere;min-width:0}
.kind-badge{font-size:11px;font-weight:600;letter-spacing:1px;text-transform:uppercase;
padding:4px 9px;border-radius:7px;border:1px solid var(--line2);color:var(--muted);background:var(--panel)}
.kind-keyword{color:var(--c-key);border-color:rgba(255,126,182,.4)}
.kind-type{color:var(--c-type);border-color:rgba(124,245,196,.4)}
.kind-phase{color:var(--blue);border-color:rgba(130,170,255,.4)}
.kind-method,.kind-builtin{color:var(--c-fn);border-color:rgba(130,170,255,.4)}
.kind-annotation{color:var(--c-annot);border-color:rgba(199,146,234,.4)}
.kind-namespace{color:var(--amber);border-color:rgba(246,193,119,.4)}
.kind-operator{color:var(--muted)}
.sig{display:block;margin:16px 0 4px;color:var(--c-fn);background:var(--c-bg);
border:1px solid var(--line);border-radius:9px;padding:9px 12px;font-size:13.5px;
overflow-x:auto;white-space:nowrap;-webkit-overflow-scrolling:touch}
.desc{margin-top:14px}
.desc p{margin:0 0 12px;font-size:15.5px;color:var(--text);max-width:720px}
.desc code{color:var(--amber);background:var(--panel);padding:1px 5px;border-radius:5px;font-size:13px;overflow-wrap:anywhere}
.desc b{color:var(--head)}
.params,.examples,.related{margin-top:28px}
.params h2,.examples h2,.related h2{font-size:14px;text-transform:uppercase;letter-spacing:1.5px;
color:var(--muted);font-weight:600;margin:0 0 12px}
.param{display:flex;flex-direction:column;gap:4px;padding:11px 13px;border:1px solid var(--line);
border-radius:10px;background:var(--panel);margin-bottom:8px;scroll-margin-top:calc(var(--hdr) + 16px)}
.param .pname{color:var(--c-arg);font-size:13.5px;flex:none}
.param .pdesc{color:var(--text);font-size:14.5px}
.param code{color:var(--amber);background:var(--c-bg);padding:1px 5px;border-radius:5px;font-size:12.5px}
.rel-row{display:flex;flex-wrap:wrap;gap:8px}
a.rel{font-family:"JetBrains Mono",monospace;font-size:13px;color:var(--text);background:var(--panel);
border:1px solid var(--line);border-radius:9px;padding:8px 12px}
a.rel:active,a.rel:hover{border-color:var(--mint);color:var(--mint)}
a.back{display:inline-block;margin-top:30px;color:var(--muted);font-size:14px}
a.back:active,a.back:hover{color:var(--mint)}
/* namespace overview */
.ns-blurb{color:var(--muted);font-size:15.5px;max-width:720px;margin:14px 0 22px}
.ns-blurb code{color:var(--mint);background:rgba(124,245,196,.08);padding:1px 5px;border-radius:5px;font-size:13px}
.ns-methods{display:flex;flex-direction:column;gap:8px}
a.ns-method{display:flex;flex-direction:column;gap:4px;padding:12px 14px;border:1px solid var(--line);
border-radius:11px;background:var(--panel);transition:border-color .14s}
a.ns-method:active,a.ns-method:hover{border-color:var(--mint)}
.nm-sig{color:var(--c-fn);font-size:13px;overflow-wrap:anywhere}
.nm-tip{color:var(--muted);font-size:13.5px}
/* swatches (Color page) */
.swatch-group{margin-bottom:20px}
.swatch-group h3{margin:0 0 12px;font-size:13px;letter-spacing:1.5px;text-transform:uppercase;color:var(--muted)}
.swatch-row{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px}
.swatch{display:flex;align-items:center;gap:9px;background:var(--panel);border:1px solid var(--line);
border-radius:9px;padding:8px 10px;min-width:0;scroll-margin-top:calc(var(--hdr) + 16px)}
.chip{width:22px;height:22px;border-radius:6px;flex:none;box-shadow:inset 0 0 0 1px rgba(255,255,255,.12)}
.cname{font-family:"JetBrains Mono",monospace;font-size:12px;color:var(--head);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.chex{margin-left:auto;font-family:"JetBrains Mono",monospace;font-size:11px;color:var(--muted);flex:none}
/* ---- api index + search ---- */
main.ref{padding:24px 16px 72px;max-width:var(--max);margin:0 auto}
.ref-intro{margin-bottom:26px}
.ref-intro .kicker{font-size:13px;font-weight:600;letter-spacing:2px;text-transform:uppercase;color:var(--mint);margin-bottom:10px}
.ref-intro h1{font-size:clamp(28px,8vw,46px);margin:0 0 12px;color:var(--head);letter-spacing:-1px}
.ref-intro p{font-size:15.5px;color:var(--muted);max-width:720px;margin:0 0 18px}
.search{width:100%;max-width:560px;font-family:"JetBrains Mono",monospace;font-size:16px;color:var(--head);
background:var(--panel);border:1px solid var(--line2);border-radius:11px;padding:13px 15px;outline:none}
.search:focus{border-color:var(--mint);box-shadow:0 0 0 3px rgba(124,245,196,.12)}
.noresults{color:var(--muted);margin-top:16px}
.idx-sec{margin-top:34px;scroll-margin-top:calc(var(--hdr) + 16px)}
.idx-sec h2{font-size:20px;color:var(--head);margin:0 0 4px;letter-spacing:-.3px}
.sec-blurb{color:var(--muted);font-size:14px;margin:0 0 14px;max-width:720px}
.sec-blurb code{color:var(--mint);background:rgba(124,245,196,.08);padding:1px 5px;border-radius:5px;font-size:12.5px}
.idx-grid{display:grid;grid-template-columns:1fr;gap:8px}
a.idx-item{display:flex;flex-direction:column;gap:3px;padding:12px 13px;border:1px solid var(--line);
border-radius:10px;background:var(--panel);transition:border-color .14s;min-width:0}
a.idx-item:active,a.idx-item:hover{border-color:var(--mint)}
a.idx-item code{color:var(--c-fn);font-size:14px;overflow-wrap:anywhere}
a.idx-item span{color:var(--muted);font-size:13px;line-height:1.5}
/* ---- hover card (hover-capable devices only; JS also gates it) ---- */
.hovercard{position:fixed;z-index:100;max-width:min(340px,calc(100vw - 24px));background:var(--panel2);
border:1px solid var(--line2);border-radius:12px;padding:12px 14px;box-shadow:0 20px 50px -18px rgba(0,0,0,.8);
pointer-events:auto;cursor:pointer}
.hovercard:hover{border-color:var(--mint)}
.hc-top{display:flex;align-items:center;gap:9px;margin-bottom:8px}
.hc-name{font-family:"JetBrains Mono",monospace;color:var(--head);font-weight:700;font-size:14px;overflow-wrap:anywhere}
.hc-sig{display:block;color:var(--c-fn);background:var(--c-bg);border:1px solid var(--line);border-radius:7px;
padding:6px 9px;font-size:12px;margin-bottom:8px;white-space:pre-wrap;overflow-wrap:anywhere}
.hc-tip{color:var(--text);font-size:13.5px;line-height:1.5}
.hc-foot{color:var(--mint);font-size:11.5px;margin-top:9px;letter-spacing:.3px}
/* ---- target flash ---- */
@keyframes ludicflash{0%{background:rgba(124,245,196,.28);box-shadow:0 0 0 3px rgba(124,245,196,.28)}
100%{background:transparent;box-shadow:0 0 0 3px transparent}}
.flash{animation:ludicflash 1.4s ease-out}
.item-head.flash h1{color:var(--mint)}
/* ============ tablet / desktop ============ */
@media (min-width:640px){
:root{--hdr:64px}
.wrap{padding:0 24px}
main.item{padding:34px 24px 90px}
main.ref{padding:34px 24px 90px}
.param{flex-direction:row;gap:14px;align-items:baseline}
.param .pname{min-width:120px}
.idx-grid{grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px}
.swatch-row{grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px}
.sig{display:inline-block}
}
@media (min-width:720px){
.nav-toggle{display:none}
.nav-links{display:flex;position:static;flex-direction:row;gap:22px;margin-left:auto;
align-items:center;background:none;border:none;padding:0}
.nav-links a{color:var(--muted);font-size:14.5px;padding:0;border-top:none}
.nav-links a:hover{color:var(--head)}
.nav-links .nav-cta{padding:8px 15px;border-radius:9px;border:1px solid var(--line2);
background:var(--panel);color:var(--head)!important}
.nav-links .nav-cta:hover{border-color:var(--mint)}
}
/* keyboard/mouse only: lift affordances that would stick on touch */
@media (hover:hover){
a.ns-method:hover{transform:translateX(2px)}
a.idx-item:hover{transform:translateY(-2px)}
}

View file

@ -160,10 +160,10 @@
function flash(el){ if (!el) return; el.classList.remove("flash"); void el.offsetWidth; el.classList.add("flash"); }
function flashTarget(){
const go = () => {
// Only a deep link gets a flash. The old fallback flashed the page title
// on every plain visit, which points at nothing the reader was looking for.
const h = location.hash ? decodeURIComponent(location.hash.slice(1)) : "";
let el = h ? document.getElementById(h) : null;
if (!el) el = document.querySelector(".item-head") || document.getElementById("top");
flash(el);
if (h) flash(document.getElementById(h));
};
if (document.readyState !== "loading") go(); else document.addEventListener("DOMContentLoaded", go);
window.addEventListener("hashchange", () => { const el = document.getElementById(decodeURIComponent(location.hash.slice(1))); flash(el); });

View file

@ -1,205 +1,271 @@
:root{
--bg:#0a0d18; --bg2:#0d1122; --panel:#12172b; --panel2:#151b32;
--line:#232b47; --line2:#2c3660; --text:#c9d2ea; --head:#f2f5ff; --muted:#8290b4;
--mint:#7cf5c4; --coral:#ff5d73; --violet:#c792ea; --blue:#82aaff; --amber:#f6c177; --peri:#a6b8ff;
--c-bg:#0b0f20; --c-comment:#5b6788; --c-key:#ff7eb6; --c-type:#7cf5c4;
--c-str:#f6c177; --c-num:#a6b8ff; --c-annot:#c792ea; --c-fn:#82aaff; --c-punc:#9aa6cc;
--radius:14px; --max:1120px; --hdr:60px;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
body{margin:0;overflow-x:hidden;
background:
radial-gradient(900px 500px at 82% -8%, rgba(124,245,196,.10), transparent 60%),
radial-gradient(700px 520px at 6% 4%, rgba(255,93,115,.09), transparent 55%),
var(--bg);
color:var(--text);
font-family:"Space Grotesk", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
line-height:1.6;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
a{color:inherit;text-decoration:none}
code,pre,.mono{font-family:"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace}
.wrap{max-width:var(--max);margin:0 auto;padding:0 16px}
/* site.css — the landing page. Loaded after base.css, which owns the tokens.
*
* The page is a single column of sections separated by rules, not a stack of
* floating cards. Nothing animates on scroll, nothing glows, and the only
* saturated colour on the page is inside a code block. */
/* ---------- nav (mobile-first: hamburger) ---------- */
header.nav{position:sticky;top:0;z-index:50;backdrop-filter:blur(10px);
background:rgba(10,13,24,.92);border-bottom:1px solid var(--line)}
.nav-in{display:flex;align-items:center;gap:12px;min-height:var(--hdr)}
.brand{display:flex;align-items:center;gap:9px;font-weight:700;color:var(--head);font-size:17px;letter-spacing:.2px}
.logo{width:26px;height:26px;border-radius:7px;display:grid;place-items:center;flex:none;
background:linear-gradient(135deg,var(--mint),#3fd7a6);color:#06231a;font-weight:700;font-size:15px;
box-shadow:0 0 0 1px rgba(124,245,196,.3), 0 6px 20px -6px rgba(124,245,196,.5)}
.nav-toggle{margin-left:auto;display:inline-flex;align-items:center;justify-content:center;
width:42px;height:42px;border-radius:10px;border:1px solid var(--line2);background:var(--panel);
color:var(--head);font-size:20px;cursor:pointer;line-height:1}
.nav-links{display:none;position:absolute;top:100%;left:0;right:0;flex-direction:column;
background:rgba(10,13,24,.98);border-bottom:1px solid var(--line);padding:4px 16px 10px;
box-shadow:0 20px 40px -24px rgba(0,0,0,.8)}
header.nav.open .nav-links{display:flex}
.nav-links a{color:var(--text);font-size:16px;font-weight:500;padding:14px 4px;border-top:1px solid var(--line)}
.nav-links a:first-child{border-top:none}
.nav-links a:active{color:var(--mint)}
.nav-links .nav-cta{background:none;text-align:left;color:var(--text)!important}
section { padding: 56px 0 }
section + section { border-top: 1px solid var(--rule) }
/* ---------- hero ---------- */
.hero{padding:40px 0 40px;position:relative;overflow:hidden}
.pill{display:inline-flex;align-items:center;gap:9px;font-size:12.5px;color:var(--mint);
border:1px solid rgba(124,245,196,.28);background:rgba(124,245,196,.06);
padding:6px 12px;border-radius:999px;font-weight:500;margin-bottom:22px}
.pill .dot{width:7px;height:7px;border-radius:50%;background:var(--mint);box-shadow:0 0 8px var(--mint)}
h1{font-size:clamp(32px,9vw,68px);line-height:1.05;margin:0 0 18px;color:var(--head);font-weight:700;letter-spacing:-1px}
h1 .accent{background:linear-gradient(120deg,var(--mint),var(--blue));-webkit-background-clip:text;
background-clip:text;-webkit-text-fill-color:transparent}
.lead{font-size:clamp(16px,4.5vw,21px);color:var(--muted);max-width:640px;margin:0 0 28px}
.lead b{color:var(--text);font-weight:600}
.cta-row{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:9px;font-weight:600;font-size:15.5px;
padding:13px 20px;border-radius:11px;transition:transform .12s, box-shadow .15s, border-color .15s;font-family:inherit;cursor:pointer;border:1px solid transparent}
.btn:active{transform:translateY(1px)}
.btn-primary{background:linear-gradient(135deg,var(--mint),#43dcae);color:#06231a;box-shadow:0 10px 30px -10px rgba(124,245,196,.6)}
.btn-ghost{border:1px solid var(--line2);color:var(--head);background:var(--panel)}
@media (hover:hover){.btn-primary:hover{box-shadow:0 14px 38px -10px rgba(124,245,196,.75)}.btn-ghost:hover{border-color:var(--mint)}}
.hero-grid{display:grid;grid-template-columns:1fr;gap:30px;align-items:center}
.hero-grid>*{min-width:0}
/* window chrome for code */
.code-card{background:var(--c-bg);border:1px solid var(--line);border-radius:var(--radius);
overflow:hidden;min-width:0;box-shadow:0 24px 50px -30px rgba(0,0,0,.7)}
.code-top{display:flex;align-items:center;gap:8px;padding:11px 14px;border-bottom:1px solid var(--line);
background:linear-gradient(180deg,var(--panel),rgba(18,23,43,.4))}
.tl{width:11px;height:11px;border-radius:50%;flex:none}
.tl.r{background:#ff5f57}.tl.y{background:#febc2e}.tl.g{background:#28c840}
.code-name{margin-left:8px;font-size:12.5px;color:var(--muted)}
pre{margin:0;padding:16px;overflow:auto;-webkit-overflow-scrolling:touch;font-size:12.5px;line-height:1.7;tab-size:2;max-width:100%}
pre::-webkit-scrollbar{height:8px;width:8px}
pre::-webkit-scrollbar-thumb{background:var(--line2);border-radius:6px}
.t-com{color:var(--c-comment);font-style:italic}
.t-key{color:var(--c-key)}.t-type{color:var(--c-type)}.t-str{color:var(--c-str)}
.t-num{color:var(--c-num)}.t-annot{color:var(--c-annot)}.t-fn{color:var(--c-fn)}.t-punc{color:var(--c-punc)}.t-arg{color:var(--amber)}
pre a.tok{border-radius:3px;transition:background .12s, outline-color .12s}
pre a.tok:hover{background:rgba(124,245,196,.14);outline:1px solid rgba(124,245,196,.35)}
.hero { padding: 64px 0 56px }
.hero-grid { display: grid; gap: 40px; align-items: start }
@media (min-width: 940px) {
.hero-grid { grid-template-columns: 1fr 1fr; gap: 52px; align-items: center }
}
.hero h1 {
font-size: clamp(34px, 5.2vw, 52px);
line-height: 1.08;
letter-spacing: -0.02em;
margin: 0 0 18px;
text-wrap: balance;
}
.lead {
font-size: 17.5px;
color: var(--ink-2);
max-width: 54ch;
margin: 0 0 28px;
}
.lead b { color: var(--ink); font-weight: 600 }
.lead code { font-size: 0.88em }
.cta-row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center }
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: inherit;
font-size: 15px;
font-weight: 500;
padding: 10px 18px;
border-radius: var(--radius);
border: 1px solid transparent;
cursor: pointer;
}
.btn:hover { text-decoration: none }
.btn-primary { background: var(--accent); color: #fff; border-color: var(--accent) }
.btn-primary:hover { background: var(--accent-ink); border-color: var(--accent-ink) }
@media (prefers-color-scheme: dark) {
.btn-primary { color: #10202e }
}
.btn-ghost { border-color: var(--rule-2); color: var(--ink); background: var(--panel) }
.btn-ghost:hover { border-color: var(--muted) }
/* ---------- code cards ---------- */
.code-card {
background: var(--code-bg);
border: 1px solid var(--code-rule);
border-radius: var(--radius);
overflow: hidden;
}
.code-top {
display: flex;
align-items: center;
padding: 9px 14px;
border-bottom: 1px solid var(--code-rule);
background: var(--paper-2);
}
.code-name {
font-family: var(--mono);
font-size: 12px;
color: var(--muted);
}
.code-card pre {
margin: 0;
border: none;
border-radius: 0;
background: none;
}
/* ---------- pipeline ---------- */
.pipeline{display:flex;flex-direction:column;align-items:center;gap:8px;
margin:8px 0 0;padding:18px 16px;border:1px dashed var(--line2);border-radius:var(--radius);background:rgba(18,23,43,.4)}
.stage{font-family:"JetBrains Mono",monospace;font-size:13px;font-weight:500;color:var(--head);text-align:center;
background:var(--panel);border:1px solid var(--line2);padding:9px 13px;border-radius:9px;white-space:nowrap}
.stage.hl{color:var(--mint);border-color:rgba(124,245,196,.35)}
.arrow{color:var(--muted);font-size:16px;line-height:1;transform:rotate(90deg)}
/* ---------- sections ---------- */
section{padding:48px 0}
.sec-head{max-width:680px;margin-bottom:30px}
.kicker{font-size:12.5px;font-weight:600;letter-spacing:2px;text-transform:uppercase;color:var(--mint);margin-bottom:12px}
h2{font-size:clamp(25px,6.5vw,40px);margin:0 0 12px;color:var(--head);letter-spacing:-.6px;font-weight:700;line-height:1.12}
.sec-head p{font-size:16px;color:var(--muted);margin:0}
.pipeline {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 10px;
padding: 22px 0 0;
}
.stage {
font-family: var(--mono);
font-size: 12.5px;
color: var(--ink-2);
border: 1px solid var(--rule-2);
border-radius: var(--radius);
padding: 5px 11px;
background: var(--panel);
}
.stage.hl { color: var(--accent); border-color: var(--accent); background: var(--accent-bg) }
.arrow { color: var(--muted); font-size: 13px }
/* feature grid */
.feat-grid{display:grid;grid-template-columns:1fr;gap:14px}
.feat{background:linear-gradient(180deg,var(--panel),var(--bg2));border:1px solid var(--line);
border-radius:var(--radius);padding:20px 18px;transition:transform .16s, border-color .16s}
.feat .ico{width:40px;height:40px;border-radius:10px;display:grid;place-items:center;margin-bottom:14px;
background:rgba(124,245,196,.09);border:1px solid rgba(124,245,196,.2);font-size:20px}
.feat h3{margin:0 0 8px;font-size:17px;color:var(--head);font-weight:600}
.feat p{margin:0;font-size:14.5px;color:var(--muted);line-height:1.6}
.feat p code{color:var(--mint);font-size:12.5px;background:rgba(124,245,196,.08);padding:1px 5px;border-radius:5px;overflow-wrap:anywhere}
/* ---------- section headings ---------- */
.sec-head { max-width: 62ch; margin-bottom: 34px }
.kicker {
font-family: var(--mono);
font-size: 11.5px;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 10px;
}
.sec-head h2 {
font-size: clamp(24px, 3.4vw, 32px);
letter-spacing: -0.015em;
margin: 0 0 12px;
}
.sec-head p { margin: 0; color: var(--muted); font-size: 16px }
/* ---------- features ---------- */
/* The container draws the top and left edges and each cell draws its own right
and bottom, so a final row with fewer cards than columns simply stops — the
bleed-through trick (a ruled background behind gaps) leaves a grey block in
the hole instead, and the card count is not a multiple of the column count at
any breakpoint. Same construction for .editors, .idx-grid and .swatch-row. */
.feat-grid {
display: grid;
border-top: 1px solid var(--rule);
border-left: 1px solid var(--rule);
border-radius: var(--radius);
overflow: hidden;
}
@media (min-width: 620px) { .feat-grid { grid-template-columns: 1fr 1fr } }
@media (min-width: 940px) { .feat-grid { grid-template-columns: 1fr 1fr 1fr } }
.feat {
padding: 22px 22px 24px;
border-right: 1px solid var(--rule);
border-bottom: 1px solid var(--rule);
}
.feat h3 {
font-family: var(--sans);
font-size: 15px;
font-weight: 600;
letter-spacing: 0;
margin: 0 0 7px;
}
.feat p { margin: 0; font-size: 14.5px; color: var(--muted) }
.feat code { font-size: 0.87em }
/* ---------- showcase ---------- */
.tabs { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 16px }
.tab {
font-family: var(--mono);
font-size: 12.5px;
color: var(--muted);
background: none;
border: 1px solid transparent;
border-radius: var(--radius);
padding: 6px 12px;
cursor: pointer;
}
.tab:hover { color: var(--ink) }
.tab.active {
color: var(--accent);
border-color: var(--rule-2);
background: var(--panel);
}
/* showcase tabs */
.tabs{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px}
.tab{font-family:"JetBrains Mono",monospace;font-size:13px;color:var(--muted);background:var(--panel);
border:1px solid var(--line);padding:9px 15px;border-radius:9px;cursor:pointer;transition:all .14s;font-weight:500}
.tab.active{color:#06231a;background:var(--mint);border-color:var(--mint);font-weight:600}
.panel-code { display: none }
.panel-code.active { display: block }
.showcase-note{margin-top:16px;font-size:14px;color:var(--muted);display:flex;gap:10px;align-items:flex-start}
.showcase-note .b{color:var(--mint);font-weight:700;flex:none}
.panel-code pre { max-height: 62vh }
/* steps */
.steps{display:grid;grid-template-columns:1fr;gap:24px;align-items:start}
.step{display:flex;gap:14px;margin-bottom:20px}
.step .n{flex:none;width:32px;height:32px;border-radius:9px;display:grid;place-items:center;font-weight:700;
font-family:"JetBrains Mono",monospace;font-size:14px;color:var(--mint);
background:rgba(124,245,196,.08);border:1px solid rgba(124,245,196,.25)}
.step h4{margin:2px 0 6px;color:var(--head);font-size:16px;font-weight:600}
.step p{margin:0;color:var(--muted);font-size:14.5px}
.step p code{color:var(--text);background:var(--panel);padding:1px 6px;border-radius:5px;font-size:12.5px;border:1px solid var(--line);overflow-wrap:anywhere}
.term{background:var(--c-bg);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;min-width:0}
.term pre{font-size:12.5px}
.term .prompt{color:var(--mint)}
.term .out{color:var(--muted)}
/* editors strip */
.editors{display:flex;flex-wrap:wrap;gap:10px}
.ed{font-family:"JetBrains Mono",monospace;font-size:13.5px;color:var(--text);background:var(--panel);
border:1px solid var(--line);border-radius:10px;padding:10px 14px;display:flex;align-items:center;gap:9px}
.ed .k{color:var(--mint)}
/* philosophy banner */
.banner{background:linear-gradient(120deg,rgba(124,245,196,.08),rgba(130,170,255,.06));
border:1px solid var(--line2);border-radius:18px;padding:26px 22px;display:grid;grid-template-columns:1fr;gap:26px;align-items:center}
.banner h2{font-size:clamp(22px,5.5vw,32px)}
.banner p{color:var(--muted);margin:0 0 6px;font-size:15px}
.stat-row{display:flex;gap:12px;flex-wrap:wrap}
.stat{background:var(--c-bg);border:1px solid var(--line);border-radius:12px;padding:14px 16px;flex:1 1 120px;min-width:120px}
.stat .big{font-family:"JetBrains Mono",monospace;font-size:24px;color:var(--mint);font-weight:700;line-height:1}
.stat .lbl{font-size:12.5px;color:var(--muted);margin-top:8px}
/* footer */
footer{border-top:1px solid var(--line);padding:34px 0 48px;margin-top:16px}
.foot-in{display:flex;flex-direction:column;justify-content:space-between;gap:20px;flex-wrap:wrap}
.foot-in .muted{color:var(--muted);font-size:14px}
.foot-links{display:flex;gap:18px;flex-wrap:wrap}
.foot-links a{color:var(--muted);font-size:14px}
.foot-links a:hover{color:var(--mint)}
.reveal{opacity:0;transform:translateY(16px);transition:opacity .6s ease, transform .6s ease}
.reveal.in{opacity:1;transform:none}
/* hover-card + kind badges (shared with the reference; hover-devices only) */
.hovercard{position:fixed;z-index:100;max-width:min(340px,calc(100vw - 24px));background:var(--panel2);
border:1px solid var(--line2);border-radius:12px;padding:12px 14px;box-shadow:0 20px 50px -18px rgba(0,0,0,.8);
pointer-events:auto;cursor:pointer}
.hovercard:hover{border-color:var(--mint)}
.hc-top{display:flex;align-items:center;gap:9px;margin-bottom:8px}
.hc-name{font-family:"JetBrains Mono",monospace;color:var(--head);font-weight:700;font-size:14px;overflow-wrap:anywhere}
.hc-sig{display:block;color:var(--c-fn);background:var(--c-bg);border:1px solid var(--line);border-radius:7px;
padding:6px 9px;font-size:12px;margin-bottom:8px;white-space:pre-wrap;overflow-wrap:anywhere}
.hc-tip{color:var(--text);font-size:13.5px;line-height:1.5}
.hc-foot{color:var(--mint);font-size:11.5px;margin-top:9px;letter-spacing:.3px}
.kind-badge{font-size:11px;font-weight:600;letter-spacing:1px;text-transform:uppercase;padding:3px 7px;
border-radius:6px;border:1px solid var(--line2);color:var(--muted);background:var(--panel)}
.kind-keyword{color:var(--c-key)}.kind-type{color:var(--c-type)}.kind-phase{color:var(--blue)}
.kind-method,.kind-builtin{color:var(--c-fn)}.kind-annotation{color:var(--c-annot)}.kind-namespace{color:var(--amber)}
/* ============ tablet / desktop ============ */
@media (min-width:600px){
.feat-grid{grid-template-columns:repeat(2,1fr);gap:16px}
.cta-row .btn{min-width:0}
.showcase-note {
display: flex;
gap: 10px;
margin-top: 16px;
font-size: 14.5px;
color: var(--muted);
max-width: 74ch;
}
@media (min-width:720px){
:root{--hdr:64px}
.wrap{padding:0 24px}
.nav-toggle{display:none}
.nav-links{display:flex;position:static;flex-direction:row;gap:24px;margin-left:auto;align-items:center;background:none;border:none;padding:0;box-shadow:none}
.nav-links a{color:var(--muted);font-size:14.5px;padding:0;border-top:none}
.nav-links a:hover{color:var(--head)}
.nav-links .nav-cta{padding:8px 15px;border-radius:9px;border:1px solid var(--line2);
background:var(--panel);color:var(--head)!important}
.nav-links .nav-cta:hover{border-color:var(--mint)}
.pipeline{flex-direction:row;flex-wrap:wrap;justify-content:center;gap:4px}
.stage{font-size:12.5px;padding:8px 12px}
.arrow{transform:none;padding:0 6px;font-size:14px}
.showcase-note .b { color: var(--rule-2) }
/* ---------- philosophy ---------- */
.banner { display: grid; gap: 28px }
@media (min-width: 860px) { .banner { grid-template-columns: 1.5fr 1fr; gap: 48px } }
.banner h2 { font-size: clamp(22px, 3vw, 28px); margin: 0 0 14px }
.banner p { color: var(--muted); font-size: 15.5px; max-width: 60ch }
.stat-row { display: grid; gap: 18px; align-content: start }
.stat { border-left: 2px solid var(--rule-2); padding-left: 14px }
.stat .big {
font-family: var(--serif);
font-size: 26px;
font-weight: 600;
color: var(--ink);
line-height: 1.1;
}
@media (min-width:900px){
.hero{padding:72px 0 52px}
section{padding:68px 0}
.hero-grid{grid-template-columns:1.02fr .98fr;gap:44px}
.feat-grid{grid-template-columns:repeat(3,1fr);gap:18px}
.steps{grid-template-columns:1.1fr 1fr;gap:40px}
.term{position:sticky;top:88px}
.banner{grid-template-columns:1.2fr 1fr;padding:36px 32px;gap:34px}
.foot-in{flex-direction:row;align-items:center}
.stat .lbl { font-size: 13.5px; color: var(--muted); margin-top: 2px }
/* ---------- get started ---------- */
.steps { display: grid; gap: 34px }
@media (min-width: 940px) { .steps { grid-template-columns: 1fr 1fr; gap: 48px; align-items: start } }
.step { display: flex; gap: 14px; padding: 16px 0; border-top: 1px solid var(--rule) }
.step:first-child { border-top: none; padding-top: 0 }
.step .n {
flex: none;
width: 22px; height: 22px;
display: grid; place-items: center;
border-radius: 50%;
border: 1px solid var(--rule-2);
font-family: var(--mono);
font-size: 11.5px;
color: var(--muted);
margin-top: 2px;
}
@media (hover:hover){
.feat:hover{transform:translateY(-3px);border-color:var(--line2)}
.tab:hover{color:var(--text);border-color:var(--line2)}
.step h4 { font-family: var(--sans); font-size: 15px; font-weight: 600; margin: 0 0 4px }
.step p { margin: 0; font-size: 14.5px; color: var(--muted) }
.term {
background: var(--code-bg);
border: 1px solid var(--code-rule);
border-radius: var(--radius);
overflow: hidden;
position: sticky;
top: calc(var(--hdr) + 20px);
}
.term pre { margin: 0; border: none; border-radius: 0; background: none }
.term .prompt { color: var(--muted); user-select: none }
.term .out { color: var(--c-type) }
/* ---------- editors ---------- */
.editors {
display: grid;
border-top: 1px solid var(--rule);
border-left: 1px solid var(--rule);
border-radius: var(--radius);
overflow: hidden;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
.ed {
padding: 13px 16px;
font-size: 14.5px;
color: var(--ink-2);
border-right: 1px solid var(--rule);
border-bottom: 1px solid var(--rule);
}
.ed .k { color: var(--rule-2); margin-right: 4px }
/* the second half of the headline, set back so the first clause carries it */
.accent { color: var(--muted) }
.pipe-note {
text-align: center;
color: var(--muted);
font-size: 13.5px;
margin: 12px auto 0;
max-width: 62ch;
}
.ed-note { margin-top: 18px; color: var(--muted); font-size: 14.5px }

View file

@ -515,10 +515,12 @@ function j_has(v: JVal, key: pointer) -> bool {
return false
}
# The stylesheets are linked, not inlined: base.css alone is ~6 KB, and there
# are 900+ pages, so inlining cost ~10 MB of duplicated bytes and meant a design
# change had to be regenerated to be seen. Linking makes the browser cache them
# once across the whole site. Fonts are the platform's own — no webfont request.
function emit_head(b: Sb, title: pointer, desc: pointer, cssfile: pointer) -> void {
sb_puts(b, `<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n<title>{hesc(title)}</title>\n<meta name="description" content="{hescattr(desc)}">\n<link rel="preconnect" href="https://fonts.googleapis.com">\n<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>\n<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">\n<style>\n`)
sb_puts(b, read_asset(cssfile))
sb_puts(b, `\n</style>\n</head>`)
sb_puts(b, `<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n<title>{hesc(title)}</title>\n<meta name="description" content="{hescattr(desc)}">\n<link rel="stylesheet" href="base.css">\n<link rel="stylesheet" href="{cssfile}">\n</head>`)
}
# nav links; a link whose href equals cta_href gets the nav-cta class
@ -588,7 +590,7 @@ function render_related(e: Entry, items: JVal) -> pointer {
}
function render_item(b: Sb, e: Entry, s: Section, cfg: JVal, items: JVal) -> void {
emit_head(b, e.name + " — Ludic", e.tip, "item.css")
emit_head(b, e.name + " — Ludic", e.tip, "docs.css")
sb_puts(b, "\n<body>\n")
topbar(b, cfg)
sb_putc(b, '\n')
@ -621,7 +623,7 @@ function render_item(b: Sb, e: Entry, s: Section, cfg: JVal, items: JVal) -> voi
# ---- namespace overview page ------------------------------------------------
function render_ns(b: Sb, nsname: pointer, s: Section, cfg: JVal) -> void {
emit_head(b, nsname + " — Ludic", plain_text(s.blurb), "item.css")
emit_head(b, nsname + " — Ludic", plain_text(s.blurb), "docs.css")
sb_puts(b, "\n<body>\n")
topbar(b, cfg)
sb_putc(b, '\n')
@ -647,7 +649,7 @@ function render_ns(b: Sb, nsname: pointer, s: Section, cfg: JVal) -> void {
# ---- color page (from the in-memory palette) --------------------------------
function render_color(b: Sb, cfg: JVal) -> void {
palette_init()
emit_head(b, "Color — Ludic", "The Ludic named-color palette.", "item.css")
emit_head(b, "Color — Ludic", "The Ludic named-color palette.", "docs.css")
sb_puts(b, "\n<body>\n")
topbar(b, cfg)
sb_putc(b, '\n')
@ -686,7 +688,7 @@ function render_color(b: Sb, cfg: JVal) -> void {
# ---- API index --------------------------------------------------------------
function render_api(b: Sb, sections: []Section, cfg: JVal) -> void {
emit_head(b, "Ludic — API Reference", "The complete Ludic API Reference: every keyword, type, builtin, namespace, annotation and color, each on its own page, with fuzzy search.", "item.css")
emit_head(b, "Ludic — API Reference", "The complete Ludic API Reference: every keyword, type, builtin, namespace, annotation and color, each on its own page, with fuzzy search.", "docs.css")
sb_puts(b, "\n<body>\n")
topbar(b, cfg)
sb_putc(b, '\n')
@ -775,7 +777,7 @@ function build_feats(feats: JVal) -> pointer {
while i < len(cards.kids) {
let c = cards.kids[i]
i += 1
sb_puts(b, `<div class="feat reveal"><div class="ico">{js_get(c,"icon")}</div><h3>{js_get(c,"title")}</h3><p>{js_get(c,"html")}</p></div>`)
sb_puts(b, `<div class="feat"><h3>{js_get(c,"title")}</h3><p>{js_get(c,"html")}</p></div>`)
}
return sb_str(b)
}
@ -806,7 +808,7 @@ function build_steps(start: JVal) -> pointer {
var i = 0
while i < len(st.kids) {
let s = st.kids[i]
sb_puts(b, `<div class="step reveal"><div class="n">{string(i + 1)}</div><div><h4>{js_get(s,"title")}</h4><p>{js_get(s,"html")}</p></div></div>`)
sb_puts(b, `<div class="step"><div class="n">{string(i + 1)}</div><div><h4>{js_get(s,"title")}</h4><p>{js_get(s,"html")}</p></div></div>`)
i += 1
}
return sb_str(b)
@ -881,7 +883,6 @@ function render_index(b: Sb, cfg: JVal) -> void {
let scta = j_get(hero, "secondary_cta")
let mk = new []pointer
let mv = new []pointer
put(mk, mv, "css", read_asset("site.css"))
put(mk, mv, "brand", hesc(js_get(cfg, "brand")))
put(mk, mv, "tagline", hesc(js_get(cfg, "tagline")))
put(mk, mv, "repo", js_get(cfg, "repo_url"))
@ -1002,6 +1003,10 @@ function cmd_docs_gen() -> int {
let ib = sb_new()
render_index(ib, cfg)
write_file(`{out}/index.html`, sb_str(ib))
# the stylesheets ship as files, linked from every page (see emit_head)
write_file(`{out}/base.css`, read_asset("base.css"))
write_file(`{out}/site.css`, read_asset("site.css"))
write_file(`{out}/docs.css`, read_asset("docs.css"))
write_file(`{out}/ludic-highlight.js`, render_highlighter(syms.sym))
let root = jvo()
jvput(root, "items", syms.items)