Compare commits

...

4 Commits

Author SHA1 Message Date
1cb6ad1a59 Point freeroute links at public PyPI, fix routing-config defaults
Some checks failed
CI / Lint and Format (push) Has been cancelled
CI / Test Python 3.11 on macos-latest (push) Has been cancelled
CI / Test Python 3.12 on macos-latest (push) Has been cancelled
CI / Test Python 3.13 on macos-latest (push) Has been cancelled
CI / Test Python 3.10 on ubuntu-latest (push) Has been cancelled
CI / Test Python 3.11 on ubuntu-latest (push) Has been cancelled
CI / Test Python 3.12 on ubuntu-latest (push) Has been cancelled
CI / Test Python 3.13 on ubuntu-latest (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / Build Package (push) Has been cancelled
The docs site is public; the freeroute Gitea repo is private, so link the
public PyPI project page instead. Set freeroute_shove/diagonal defaults to
false (their actual mckicad routing_config defaults) rather than
'engine-dependent'.
2026-07-14 22:53:58 -06:00
349616b77d Update routing docs for freeroute; demote FreeRouting JAR to fallback 2026-07-14 22:52:21 -06:00
fd81e63f1d Add autorouting guide for the freeroute pipeline 2026-07-14 22:52:16 -06:00
847e38e8a3 Refresh docs-site from feature/lightning-talk
Bring the docs-site tree up to date with the richer version built on the
feature/lightning-talk line (guides, expanded reference, landing page) as the
base for adding freeroute/routing documentation. Docs-site only; no source
code from that branch is merged.
2026-07-14 22:47:59 -06:00
11 changed files with 742 additions and 14 deletions

View File

@ -38,6 +38,7 @@ export default defineConfig({
{ label: "Netlist Import", slug: "guides/netlist" },
{ label: "BOM Management", slug: "guides/bom" },
{ label: "Design Rule Checks", slug: "guides/drc" },
{ label: "Autorouting", slug: "guides/routing" },
{ label: "Board Analysis", slug: "guides/analysis" },
{ label: "Export & Manufacturing", slug: "guides/export" },
{ label: "Prompt Templates", slug: "guides/prompts" },
@ -59,6 +60,7 @@ export default defineConfig({
{ label: "Troubleshooting", slug: "development/troubleshooting" },
],
},
{ label: "Lightning Talk", link: "/lightning-talk/" },
],
head: [
{

View File

@ -29,7 +29,7 @@ src/mckicad/
drc.py # DRC checking + manufacturing constraints
bom.py # BOM generation and export
export.py # Gerber, drill, PDF, SVG via kicad-cli
routing.py # FreeRouting autorouter integration
routing.py # Autorouter integration (freeroute CLI, JAR fallback)
analysis.py # Board validation + real-time analysis
pcb.py # IPC-based PCB manipulation via kipy
resources/
@ -42,7 +42,7 @@ src/mckicad/
path_validator.py # Path security / directory traversal prevention
secure_subprocess.py # Safe subprocess execution with timeouts
ipc_client.py # kipy IPC wrapper for live KiCad connection
freerouting.py # FreeRouting JAR engine
freerouting.py # Autorouter engine: prefers freeroute CLI, falls back to FreeRouting JAR; native headless SES applier
file_utils.py # Project file discovery
kicad_utils.py # KiCad path detection, project search
sexp_parser.py # S-expression parsing for pin position resolution

View File

@ -26,7 +26,7 @@ Tools degrade gracefully: if KiCad is not running, IPC-dependent tools report th
**Export and manufacturing** -- Generate Gerber files, drill files, PDFs, and SVGs for your boards. Everything goes through kicad-cli with proper layer selection and output directory organization.
**Autorouting** -- Route PCBs automatically via FreeRouting integration with configurable strategies (conservative, balanced, aggressive) and technology profiles.
**Autorouting** -- Route PCBs automatically through a fully headless, Java-free pipeline built on [freeroute](https://pypi.org/project/freeroute/) (a native-Python port of the FreeRouting engine), with the FreeRouting JAR available as a fallback for maximum net completion on dense boards.
**Board analysis** -- Live board statistics, connectivity monitoring, component detail inspection, and routing quality analysis through the IPC API.

View File

@ -28,7 +28,21 @@ These settings control where the server looks for KiCad projects:
|---------------------|-------------|---------|---------|
| `KICAD_APP_PATH` | Path to the KiCad installation | Auto-detected per platform | `/Applications/KiCad/KiCad.app` |
| `KICAD_CLI_PATH` | Explicit path to kicad-cli | Auto-detected | `/usr/bin/kicad-cli` |
| `FREEROUTING_JAR_PATH` | Path to FreeRouting JAR | Auto-detected in common locations | `~/freerouting.jar` |
| `FREEROUTE_CLI` | Path to the freeroute CLI (preferred autorouter) | Auto-discovered on PATH | `~/.local/bin/freeroute` |
| `FREEROUTING_JAR_PATH` | Path to the FreeRouting JAR (optional fallback) | Auto-detected in common locations | `~/freerouting.jar` |
### Routing options
Autorouting is configured per call through the `routing_config` argument of `route_pcb_automatically`, not through environment variables. The freeroute-specific keys are:
| Key | Description | Default |
|-----|-------------|---------|
| `freeroute_engine` | Router engine: `room`, `exact`, or `grid` | `room` |
| `freeroute_pack` | Enable channel packing for tighter routing | `true` |
| `freeroute_shove` | Allow shoving existing traces to make room (`room`/`exact` engines) | `false` |
| `freeroute_diagonal` | Allow 45-degree / diagonal trace shortening (`exact` engine) | `false` |
See the [Autorouting guide](/guides/routing/) for what each engine does and when to reach for it.
### Server settings

View File

@ -101,15 +101,29 @@ List all my KiCad projects
If the server is running and configured correctly, it will scan your search paths and return a list of `.kicad_pro` files.
## Optional: FreeRouting (autorouting)
## Optional: autorouting
For automated PCB routing, install FreeRouting:
For automated PCB routing, install [freeroute](https://pypi.org/project/freeroute/), a native-Python, Java-free autorouter:
```bash
uv tool install freeroute
# or: pip install freeroute
```
That is the whole setup. freeroute is pure standard library with no dependencies and no Java runtime. mckicad auto-discovers the `freeroute` command on your PATH; if it lives somewhere unusual, set `FREEROUTE_CLI` in your `.env` to the executable path. Verify with the `check_routing_capability` tool.
See the [Autorouting guide](/guides/routing/) for the full pipeline, engine choices, and honest scope.
### Fallback: FreeRouting JAR
freeroute completes fewer nets than the original FreeRouting engine on dense commercial boards (it drops a net rather than emit a DRC violation). If you need maximum completion on a hard board and have Java available, keep the FreeRouting JAR as a fallback:
1. Download the JAR from [freerouting.app](https://freerouting.app/)
2. Place it at one of the auto-detected paths: `~/freerouting.jar`, `/usr/local/bin/freerouting.jar`, or `/opt/freerouting/freerouting.jar`
3. Install a Java runtime (`java` must be on your PATH)
4. Verify with the `check_routing_capability` tool
5. Or set `FREEROUTING_JAR_PATH` in your `.env` file to an explicit path
4. Or set `FREEROUTING_JAR_PATH` in your `.env` file to an explicit path
mckicad prefers freeroute when it is installed and falls back to the JAR only when freeroute is missing or cannot route the board.
## Development commands

View File

@ -0,0 +1,125 @@
---
title: "Autorouting"
description: "Route PCBs automatically through a headless, Java-free pipeline"
---
The routing tools take an unrouted (or partially routed) PCB and lay copper for the remaining nets automatically. The whole pipeline is headless and needs no display, no running KiCad GUI, and no Java runtime by default, which makes it usable in CI, containers, and from an MCP client.
Routing runs through [freeroute](https://pypi.org/project/freeroute/), a native-Python port of the FreeRouting engine, with the original FreeRouting JAR available as an optional fallback for dense boards.
## Prerequisites
- KiCad with its bundled `pcbnew` Python module (used for DSN export and the native SES applier)
- [freeroute](https://pypi.org/project/freeroute/) installed and on your PATH:
```bash
uv tool install freeroute
# or: pip install freeroute
```
freeroute is pure Python standard library with no dependencies. mckicad auto-discovers the `freeroute` command; if it is installed off-PATH, set `FREEROUTE_CLI` to the executable in your `.env`. Confirm your setup before routing:
```
Check whether automated routing is available for my board
```
The `check_routing_capability` tool reports which router it found (freeroute, the JAR, or both) and whether the board is ready to route.
## The routing pipeline
When you route a board, mckicad runs a fully headless sequence:
1. **Export to DSN.** The board is exported to a Specctra DSN file using KiCad's bundled `pcbnew` Python. KiCad 10's `kicad-cli` dropped the DSN export and SES import subcommands, so mckicad does this itself rather than shelling out to the CLI.
2. **Route.** freeroute reads the DSN, routes the nets, and writes a Specctra SES file. No Java, no GUI.
3. **Apply the SES natively.** A native, headless SES applier writes the routed copper straight back into the `.kicad_pcb` file. There is no GUI import step and no display required.
4. **Validate.** Run a DRC check and inspect the routing result to confirm the board is clean.
To run it:
```
Automatically route my PCB at /path/to/project.kicad_pcb
```
The `route_pcb_automatically` tool orchestrates the whole pipeline and returns routing statistics along with the path to the updated board.
## Choosing an engine
freeroute ships three engines. mckicad defaults to `room` with channel packing enabled, which is a good all-round starting point. Select an engine through the `routing_config` argument (see [Configuration](/getting-started/configuration/)).
| Engine | What it does | Reach for it when |
|--------|--------------|-------------------|
| `room` | Continuous free-space router with channel packing and shove. DRC-clean output. | Default. Best general-purpose choice for most boards. |
| `exact` | Orthogonal router whose output is verified exactly DRC-clean. Supports shove and 45-degree / diagonal shortening. | You want provably clean, predictable orthogonal routing. |
| `grid` | Fast maze router, grid-quantised. | Quick first passes and simpler boards where speed matters more than density. |
The relevant `routing_config` keys are:
- `freeroute_engine` -- `room` (default), `exact`, or `grid`
- `freeroute_pack` -- enable channel packing for tighter routing (default `true`)
- `freeroute_shove` -- allow shoving existing traces to make room
- `freeroute_diagonal` -- allow 45-degree / diagonal trace shortening
## DRC-clean output
freeroute is built to never emit a design rule violation. Every engine produces DRC-clean copper, and `exact` verifies its output as exactly clean. The trade-off is deliberate: when the router cannot place a net without breaking clearance, it **drops the net rather than route it into a violation**. You get a board that passes DRC with some nets still unrouted, not a fully connected board that fails DRC.
After routing, verify the result:
```
Analyze the routing quality of my board
```
`analyze_routing_quality` reports on trace characteristics, via usage, and potential issues. To see how many nets actually got connected:
```
Check the routing connectivity of my PCB
```
`check_connectivity` reports total nets, routed vs unrouted counts, and the routing completion percentage. Any nets freeroute dropped show up here as unrouted, so you can decide whether to reroute with different settings, route them by hand, or reach for the JAR fallback.
## Fallback: the FreeRouting JAR
If freeroute is not installed, or it cannot route a particular board, mckicad falls back to the original FreeRouting JAR. This path requires a Java runtime and the JAR on disk (see [Installation](/getting-started/installation/)). The JAR reaches higher net completion on dense commercial boards, at the cost of pulling Java back into the pipeline.
mckicad picks the router for you: freeroute when it is available and can route the board, the JAR otherwise. You do not have to choose manually, but you can keep both installed so the fallback is there when you need it.
## What freeroute is and is not
freeroute is **Alpha**. Being honest about its scope saves surprises:
**What it does today:** routes real KiCad boards across multiple layers with vias, rip-up and retry, channel packing, and 45-degree shortening, and it produces DRC-clean output.
**What it does not do yet:** it is **not** at FreeRouting/JAR density parity on dense commercial boards. Because it drops a net rather than emit a DRC violation, it completes fewer nets than the JAR on a hard board.
**Recommendation:** use freeroute for headless, JVM-free routing -- CI, containers, and MCP-driven workflows where you cannot or do not want to run Java. Keep the FreeRouting JAR as a fallback when you need maximum net completion on a dense board.
## Routing tools
| Tool | Description |
|------|-------------|
| `check_routing_capability` | Check whether automated routing is available and which router was found |
| `route_pcb_automatically` | Run the full headless routing pipeline (freeroute, JAR fallback) |
| `analyze_routing_quality` | Analyze the routed board for quality and potential issues |
| `check_connectivity` | Report routed vs unrouted nets and completion percentage |
## Troubleshooting
### No router found
1. Confirm freeroute is installed: `freeroute --version`
2. If it is installed off-PATH, set `FREEROUTE_CLI` to the executable path
3. Run `check_routing_capability` to see what mckicad detected
4. As a fallback, install the FreeRouting JAR and a Java runtime (see [Installation](/getting-started/installation/))
### Some nets are unrouted
1. This is expected behaviour: freeroute drops nets it cannot route cleanly rather than create DRC violations
2. Try a different engine (`exact` or `grid`) or adjust `freeroute_pack` / `freeroute_shove` / `freeroute_diagonal`
3. Route the remaining nets by hand in KiCad
4. For a dense board, install the FreeRouting JAR and let mckicad fall back to it for higher completion
### DSN export or SES apply fails
1. Ensure KiCad's bundled `pcbnew` Python module is available to the server
2. Verify the `.kicad_pcb` file opens cleanly in KiCad
3. Check the server logs (`mckicad.log`) for the exact failure

View File

@ -75,3 +75,9 @@ mckicad just gives them a voice.
description="Install mckicad and connect it to your first KiCad project."
href="/getting-started/installation/"
/>
<LinkCard
title="Lightning talk: teaching a language model to draw schematics"
description="Ninety seconds on why kicad-sch-api had to go, the SchDocument engine that replaced it, and the synth schematic it drew with zero ERC errors."
href="/lightning-talk/"
/>

View File

@ -58,9 +58,23 @@ KICAD_APP_PATH=/Applications/KiCad/KiCad.app
- Windows: `C:\Program Files\KiCad`
- Linux: `/usr/share/kicad`
### FREEROUTE_CLI
Path to the [freeroute](https://pypi.org/project/freeroute/) CLI, the preferred autorouter. freeroute is a native-Python, Java-free port of the FreeRouting engine.
```
FREEROUTE_CLI=~/.local/bin/freeroute
```
**Default:** Auto-discovered on your system PATH. Set this only if the `freeroute` executable is installed in a location that is not on PATH. Install it with `uv tool install freeroute` or `pip install freeroute`.
Used by: `route_pcb_automatically`, `check_routing_capability`.
Per-call engine and packing behaviour is controlled through the `routing_config` argument (`freeroute_engine`, `freeroute_pack`, `freeroute_shove`, `freeroute_diagonal`), not through environment variables. See the [Autorouting guide](/guides/routing/).
### FREEROUTING_JAR_PATH
Path to the FreeRouting JAR file for automated PCB routing.
Path to the FreeRouting JAR file. This is the optional fallback autorouter, used only when freeroute is not installed or cannot route a given board. mckicad prefers freeroute (see `FREEROUTE_CLI`).
```
FREEROUTING_JAR_PATH=~/freerouting.jar
@ -71,7 +85,7 @@ FREEROUTING_JAR_PATH=~/freerouting.jar
- `/usr/local/bin/freerouting.jar`
- `/opt/freerouting/freerouting.jar`
Requires a Java runtime (`java` must be on PATH).
Requires a Java runtime (`java` must be on PATH). Only needed for maximum net completion on dense boards; the primary freeroute path needs no Java.
### LOG_LEVEL
@ -104,7 +118,11 @@ KICAD_USER_DIR=~/Documents/KiCad
# KiCad application path
# KICAD_APP_PATH=/usr/share/kicad
# FreeRouting JAR for autorouting
# Autorouter: freeroute is preferred and auto-discovered on PATH.
# Set FREEROUTE_CLI only if it is installed off-PATH.
# FREEROUTE_CLI=~/.local/bin/freeroute
# Optional FreeRouting JAR fallback (needs Java; only for dense boards)
# FREEROUTING_JAR_PATH=~/freerouting.jar
# Logging level

View File

@ -155,12 +155,12 @@ See [Autowiring guide](/guides/autowire/) for details on the decision tree.
## Routing tools
`tools/routing.py` -- FreeRouting autorouter integration.
`tools/routing.py` -- autorouter integration. Prefers the Java-free [freeroute](https://pypi.org/project/freeroute/) CLI and falls back to the FreeRouting JAR. See the [Autorouting guide](/guides/routing/).
| Tool | Description |
|------|-------------|
| `check_routing_capability` | Check whether automated PCB routing is available |
| `route_pcb_automatically` | Automatically route a PCB using FreeRouting |
| `check_routing_capability` | Check whether automated PCB routing is available (freeroute or JAR) |
| `route_pcb_automatically` | Automatically route a PCB, headless, via freeroute (JAR fallback) |
| `analyze_routing_quality` | Analyze current PCB routing for quality and potential issues |
## PCB tools (IPC)

View File

@ -0,0 +1,176 @@
---
import "../styles/lightning-talk.css";
const title = "Teaching a language model to draw schematics";
const description =
"A 90-second lightning talk on mckicad: why I replaced kicad-sch-api with an internal SchDocument engine, and what it unlocked.";
// lucide-style icon paths (stroke, viewBox 24)
const icons = {
problem: '<path d="m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3z"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
engine: '<rect width="16" height="16" x="4" y="4" rx="2"/><rect width="6" height="6" x="9" y="9" rx="1"/><path d="M15 2v2M9 2v2M15 20v2M9 20v2M2 15h2M2 9h2M20 15h2M20 9h2"/>',
wire: '<circle cx="12" cy="4.5" r="2.5"/><circle cx="4.5" cy="19.5" r="2.5"/><circle cx="19.5" cy="19.5" r="2.5"/><path d="M10.4 6.4 6 16.5M13.6 6.4 18 16.5M7 19.5h10"/>',
check: '<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/>',
zap: '<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>',
};
const beats = [
{
n: "01",
icon: icons.problem,
tag: "The problem",
heading: "The library kept lying to me",
body: [
"mckicad is an MCP server that lets a language model design KiCad schematics on its own. For a long time it stood on <code>kicad-sch-api</code> — and that library kept returning wrong answers.",
"Global labels: dropped silently. Custom library pins: empty lists. When the layer you build on returns confident nonsense, nothing downstream can be trusted. So I did the reckless thing and ripped it out.",
],
},
{
n: "02",
icon: icons.engine,
tag: "The engine",
heading: "A parser that round-trips clean",
body: [
"The heart is <code>sexp_tree</code> — a full S-expression parser and serializer for KiCad's native format. Parse it, mutate it, write it back, byte-for-byte identical.",
"On top sits <code>SchDocument</code>: load a schematic and ask it real questions. Where is every pin. What net is this wire on. Which labels actually connect. Answers from the file itself, not a guess.",
],
},
{
n: "03",
icon: icons.wire,
tag: "Autowire",
heading: "It routes around itself",
body: [
"Place the components; autowire pulls the nets for you — with real collision detection, so wires route around each other instead of bridging into shorts.",
"It clamps stub lengths when they would overlap, offsets labels so they don't collide, and tracks power-wire stubs as obstacles. The messy part of drawing a schematic, done carefully.",
],
},
{
n: "04",
icon: icons.check,
tag: "It checks its own work",
heading: "One call, zero ambiguity",
body: [
"<code>run_schematic_erc</code> runs the full electrical rules check and reports back: zero errors — or exactly what's wrong, and where.",
"The engine grades its own homework, so the model gets a hard signal instead of a hopeful vibe.",
],
},
{
n: "05",
icon: icons.zap,
tag: "The proof",
heading: "It drew Timbre",
body: [
"It generated the phase-1 bench schematic for Timbre, a Cypress PSoC-based synthesizer. Zero ERC errors.",
"A language model, placing parts and pulling nets, for real hardware headed to a bench. That's the whole point.",
],
},
];
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title} · mckicad</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="article" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<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=JetBrains+Mono:wght@400;600;700&display=swap"
rel="stylesheet"
/>
</head>
<body class="lt">
<a class="lt-skip" href="#talk">Skip to the talk</a>
<header class="lt-hero">
<div class="lt-wrap">
<p class="lt-kicker">Lightning talk · 90 seconds</p>
<h1>{title}</h1>
<p class="lt-lede">
I ripped out the library that kept lying to me and built an engine
that tells the truth.
</p>
<div class="lt-scope" role="img" aria-label="Terminal readout: running the electrical rules check on the Timbre schematic returns zero errors and zero warnings.">
<div class="lt-scope-bar" aria-hidden="true">
<span class="lt-dot"></span><span class="lt-dot"></span
><span class="lt-dot"></span>
<span>erc · timbre-phase1.kicad_sch</span>
</div>
<div class="lt-scope-body" aria-hidden="true">
<div class="cmd">run_schematic_erc timbre-phase1.kicad_sch</div>
<div class="out">▸ 47 components · 62 nets · 128 pins</div>
<div class="out">▸ checking electrical rules…</div>
<div class="ok">✓ 0 errors · 0 warnings</div>
<div class="out">
ready<span class="lt-cursor" aria-hidden="true"></span>
</div>
</div>
</div>
<nav class="lt-actions" aria-label="Primary">
<a class="lt-btn primary" href="/getting-started/installation/">
Get started
</a>
<a class="lt-btn ghost" href="/guides/autowire/">Read the autowire guide</a>
<a
class="lt-btn ghost"
href="https://git.supported.systems/MCP/kicad-mcp"
rel="noopener">Source</a
>
</nav>
</div>
</header>
<main id="talk" class="lt-wrap">
<ol class="lt-beats">
{
beats.map((b) => (
<li class="lt-beat" data-n={b.n}>
<span class="lt-beat-tag">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
set:html={b.icon}
/>
{b.tag}
</span>
<h2>{b.heading}</h2>
{b.body.map((p) => (
<p set:html={p} />
))}
</li>
))
}
</ol>
</main>
<section class="lt-closer">
<div class="lt-wrap">
<div class="lt-stat">
1,900 lines
<small>of workaround code, deleted</small>
</div>
<blockquote>
“Replaced by an engine that just tells the truth.”
</blockquote>
<footer>
Also delivered as a 90-second spoken talk. ·
<a href="/">Back to mckicad</a>
</footer>
</div>
</section>
</body>
</html>

View File

@ -0,0 +1,373 @@
/* mckicad lightning talk standalone phosphor-terminal narrative page.
Palette locked to the docs hero SVG so it reads as part of the product. */
:root {
--lt-bg: #0a1410;
--lt-panel: #0f1a14;
--lt-grid: #1a3a2a;
--lt-wire: #2d8659;
--lt-dim: #5a9e78;
--lt-bright: #a8dbbe;
--lt-ink: #d6ece0;
--lt-mute: #7a9d8c;
--lt-mono: "JetBrains Mono", ui-monospace, "Cascadia Code", "Source Code Pro",
Menlo, Consolas, "DejaVu Sans Mono", monospace;
--lt-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
--lt-maxw: 62rem;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body.lt {
margin: 0;
background: var(--lt-bg);
color: var(--lt-ink);
font-family: var(--lt-sans);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
/* faint engineering-paper grid, echoing the hero schematic */
background-image:
linear-gradient(var(--lt-grid) 0.5px, transparent 0.5px),
linear-gradient(90deg, var(--lt-grid) 0.5px, transparent 0.5px);
background-size: 28px 28px;
background-position: center top;
}
/* faint CRT scanlines — decorative, killed for reduced-motion / it's opt-out via media */
body.lt::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 100;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15) 0,
rgba(0, 0, 0, 0.15) 1px,
transparent 1px,
transparent 3px
);
mix-blend-mode: multiply;
opacity: 0.35;
}
.lt-wrap {
max-width: var(--lt-maxw);
margin: 0 auto;
padding: 0 1.25rem;
}
a {
color: var(--lt-bright);
}
/* ── skip link ─────────────────────────────── */
.lt-skip {
position: absolute;
left: -999px;
top: 0;
background: var(--lt-wire);
color: #04120c;
padding: 0.5rem 0.9rem;
border-radius: 0 0 0.4rem 0;
font-family: var(--lt-mono);
z-index: 200;
}
.lt-skip:focus {
left: 0;
}
/* ── hero ──────────────────────────────────── */
.lt-hero {
padding: clamp(3rem, 9vw, 6.5rem) 0 clamp(2rem, 6vw, 4rem);
text-align: center;
}
.lt-kicker {
font-family: var(--lt-mono);
font-size: 0.8rem;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--lt-wire);
margin: 0 0 1.1rem;
}
.lt-hero h1 {
font-size: clamp(2.1rem, 6.5vw, 4rem);
line-height: 1.05;
letter-spacing: -0.02em;
margin: 0 auto 1.1rem;
max-width: 18ch;
color: #eafff4;
text-shadow: 0 0 24px rgba(45, 134, 89, 0.35);
}
.lt-lede {
font-size: clamp(1.05rem, 2.4vw, 1.35rem);
color: var(--lt-mute);
max-width: 46ch;
margin: 0 auto 2.25rem;
}
/* terminal "scope" panel */
.lt-scope {
max-width: 40rem;
margin: 0 auto 2rem;
text-align: left;
background: var(--lt-panel);
border: 1px solid var(--lt-grid);
border-radius: 0.7rem;
box-shadow: 0 0 0 1px rgba(45, 134, 89, 0.15),
0 24px 60px -20px rgba(0, 0, 0, 0.8);
overflow: hidden;
}
.lt-scope-bar {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 0.9rem;
border-bottom: 1px solid var(--lt-grid);
font-family: var(--lt-mono);
font-size: 0.75rem;
color: var(--lt-mute);
}
.lt-dot {
width: 0.7rem;
height: 0.7rem;
border-radius: 50%;
background: var(--lt-grid);
}
.lt-scope-bar span {
margin-left: auto;
}
.lt-scope-body {
padding: 1.1rem 1.2rem 1.3rem;
font-family: var(--lt-mono);
font-size: clamp(0.8rem, 1.9vw, 0.95rem);
line-height: 1.7;
}
.lt-scope-body .cmd {
color: var(--lt-bright);
}
.lt-scope-body .cmd::before {
content: "mckicad ";
color: var(--lt-wire);
}
.lt-scope-body .out {
color: var(--lt-dim);
}
.lt-scope-body .ok {
color: #7cf0b0;
text-shadow: 0 0 12px rgba(124, 240, 176, 0.5);
}
.lt-cursor {
display: inline-block;
width: 0.6ch;
height: 1.05em;
vertical-align: text-bottom;
background: var(--lt-bright);
margin-left: 0.15rem;
animation: lt-blink 1.1s steps(1) infinite;
}
@keyframes lt-blink {
50% {
opacity: 0;
}
}
/* action buttons */
.lt-actions {
display: flex;
gap: 0.75rem;
justify-content: center;
flex-wrap: wrap;
}
.lt-btn {
font-family: var(--lt-mono);
font-size: 0.9rem;
text-decoration: none;
padding: 0.7rem 1.25rem;
border-radius: 0.45rem;
border: 1px solid var(--lt-wire);
display: inline-flex;
align-items: center;
gap: 0.5rem;
transition: background 0.15s ease, color 0.15s ease, transform 0.15s ease;
}
.lt-btn svg {
width: 1.05em;
height: 1.05em;
}
.lt-btn.primary {
background: var(--lt-wire);
color: #04120c;
font-weight: 600;
}
.lt-btn.ghost {
color: var(--lt-bright);
background: transparent;
}
.lt-btn:hover {
transform: translateY(-1px);
}
.lt-btn.ghost:hover {
background: rgba(45, 134, 89, 0.12);
}
/* ── beats ─────────────────────────────────── */
.lt-beats {
list-style: none;
margin: 0;
padding: clamp(1rem, 4vw, 2.5rem) 0;
display: grid;
gap: clamp(1.25rem, 3vw, 2rem);
}
.lt-beat {
position: relative;
background: linear-gradient(
180deg,
rgba(15, 26, 20, 0.9),
rgba(10, 20, 16, 0.9)
);
border: 1px solid var(--lt-grid);
border-left: 3px solid var(--lt-wire);
border-radius: 0 0.7rem 0.7rem 0;
padding: clamp(1.4rem, 3.5vw, 2rem) clamp(1.3rem, 4vw, 2.4rem);
overflow: hidden;
}
/* ghosted giant numeral, SpiceBook-style */
.lt-beat::after {
content: attr(data-n);
position: absolute;
top: -0.35em;
right: 0.1em;
font-family: var(--lt-mono);
font-weight: 700;
font-size: clamp(5rem, 16vw, 9rem);
line-height: 1;
color: var(--lt-wire);
opacity: 0.07;
pointer-events: none;
}
.lt-beat-tag {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-family: var(--lt-mono);
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--lt-wire);
margin-bottom: 0.7rem;
}
.lt-beat-tag svg {
width: 1.05rem;
height: 1.05rem;
stroke: var(--lt-wire);
}
.lt-beat h2 {
font-size: clamp(1.35rem, 3.4vw, 1.9rem);
line-height: 1.15;
letter-spacing: -0.01em;
margin: 0 0 0.6rem;
color: #eafff4;
max-width: 24ch;
}
.lt-beat p {
margin: 0;
color: var(--lt-mute);
max-width: 62ch;
}
.lt-beat p + p {
margin-top: 0.7rem;
}
.lt-beat code {
font-family: var(--lt-mono);
font-size: 0.9em;
color: var(--lt-bright);
background: rgba(45, 134, 89, 0.12);
padding: 0.1rem 0.4rem;
border-radius: 0.3rem;
}
/* ── proof / closer ────────────────────────── */
.lt-closer {
text-align: center;
padding: clamp(3rem, 8vw, 5.5rem) 0 clamp(4rem, 9vw, 6rem);
border-top: 1px solid var(--lt-grid);
margin-top: clamp(1rem, 4vw, 2rem);
}
.lt-stat {
font-family: var(--lt-mono);
font-weight: 700;
font-size: clamp(3rem, 12vw, 6.5rem);
line-height: 1;
color: #eafff4;
letter-spacing: -0.03em;
text-shadow: 0 0 30px rgba(45, 134, 89, 0.4);
}
.lt-stat small {
display: block;
font-family: var(--lt-mono);
font-weight: 400;
font-size: clamp(0.85rem, 2.2vw, 1.05rem);
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--lt-wire);
margin-top: 0.6rem;
}
.lt-closer blockquote {
margin: 2rem auto 0;
max-width: 34ch;
font-size: clamp(1.15rem, 3vw, 1.6rem);
line-height: 1.35;
color: var(--lt-ink);
font-style: italic;
}
.lt-closer footer {
margin-top: 2.75rem;
font-family: var(--lt-mono);
font-size: 0.82rem;
color: var(--lt-mute);
}
.lt-closer footer a {
color: var(--lt-bright);
}
/* ── responsive niceties ───────────────────── */
@media (min-width: 46rem) {
.lt-beat {
padding-right: clamp(4rem, 14vw, 8rem);
}
}
/* ── accessibility: honour reduced motion ──── */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
.lt-cursor {
animation: none;
}
body.lt::before {
display: none;
}
.lt-btn {
transition: none;
}
}
/* clear focus rings everywhere */
a:focus-visible,
.lt-btn:focus-visible {
outline: 2px solid var(--lt-bright);
outline-offset: 3px;
border-radius: 0.3rem;
}