aboutsummaryrefslogtreecommitdiff
path: root/website/css
diff options
context:
space:
mode:
Diffstat (limited to 'website/css')
-rw-r--r--website/css/og-card.css102
-rw-r--r--website/css/site.css992
2 files changed, 1094 insertions, 0 deletions
diff --git a/website/css/og-card.css b/website/css/og-card.css
new file mode 100644
index 00000000..8bb852d7
--- /dev/null
+++ b/website/css/og-card.css
@@ -0,0 +1,102 @@
+/* ──────────────────────────────────────────────────────────────
+ Shared chassis for the OG cards.
+
+ Cards are authored as real 1200x630 HTML so they use the same fonts
+ and palette as the site and can be regenerated with a screenshot
+ instead of a design session. Tokens mirror site.css's ink ground —
+ duplicated deliberately, because a card is a frozen artifact and
+ must not change shape when the site's CSS does.
+ ────────────────────────────────────────────────────────────── */
+
+@font-face {
+ font-family: "JetBrains Mono";
+ font-style: normal;
+ font-weight: 400 700;
+ font-display: block;
+ src: url("/assets/jbm.woff2?v=2") format("woff2");
+}
+
+:root {
+ --bg: #0c0c12;
+ --panel: #111119;
+ --mass: #1a1a24;
+ --ink: #e9e6f2;
+ --dim: #a29db8;
+ --faint: #736e8a;
+ --line: #1a1a24;
+ --line2: #2a2839;
+ --spot: #cba6f7;
+ --run: #5fae74;
+ --wait: #d3a027;
+ --idle: #5a615c;
+ --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
+ --disp: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
+}
+
+* { box-sizing: border-box; }
+/* On a card page the document IS the card, so a full-page capture is the
+ finished asset at exactly 1200x630 whatever the window size. Scoped to
+ .og so the comparison viewer, which loads this same file for the tokens,
+ isn't clamped to card dimensions. */
+html.og, body.og {
+ margin: 0; padding: 0; background: var(--bg);
+ width: 1200px; height: 630px; overflow: hidden;
+}
+
+.card {
+ position: relative;
+ width: 1200px;
+ height: 630px;
+ overflow: hidden;
+ background: var(--bg);
+ color: var(--ink);
+ font-family: var(--mono);
+ -webkit-font-smoothing: antialiased;
+ background-image:
+ linear-gradient(var(--line) 1px, transparent 1px),
+ linear-gradient(90deg, var(--line) 1px, transparent 1px);
+ background-size: 60px 60px;
+}
+
+/* the ram as mass, bleeding off the right edge */
+.card .ram {
+ background: currentColor;
+ -webkit-mask: url("/assets/ram.svg") center/contain no-repeat;
+ mask: url("/assets/ram.svg") center/contain no-repeat;
+ display: block;
+}
+.card .mass {
+ position: absolute;
+ right: -110px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 620px;
+ height: 620px;
+ color: var(--mass);
+ z-index: 0;
+}
+.card .in { position: relative; z-index: 1; height: 100%; }
+
+/* lockup */
+.lockup { display: flex; align-items: center; gap: 14px; }
+.lockup .ram { width: 38px; height: 38px; color: var(--ink); }
+.lockup b {
+ font-family: var(--disp);
+ font-weight: 900;
+ font-size: 34px;
+ letter-spacing: -0.045em;
+}
+
+.eyebrow {
+ display: flex;
+ align-items: center;
+ gap: 18px;
+ color: var(--spot);
+ font-size: 19px;
+ letter-spacing: 0.3em;
+ text-transform: uppercase;
+}
+.eyebrow s { width: 56px; height: 2px; background: var(--spot); text-decoration: none; }
+
+/* keep the safe area generous: some surfaces crop toward square */
+.pad { padding: 54px 62px; }
diff --git a/website/css/site.css b/website/css/site.css
new file mode 100644
index 00000000..19506d43
--- /dev/null
+++ b/website/css/site.css
@@ -0,0 +1,992 @@
+/* ──────────────────────────────────────────────────────────────
+ site.css — herdr.dev chassis. Loaded after style.css, which is
+ kept only for the session mock (.herdr-ui-mock) and its tokens.
+
+ Ink is the default ground and the presentation layer; paper is an opt-in
+ preference shared with the docs. html[data-palette] themes the mock only
+ and follows the ground, so page and plate always share one mauve.
+ ────────────────────────────────────────────────────────────── */
+
+/* Ink is the presentation layer: terminals are dark and the audience has
+ internalised that, so it is the default and never follows the system.
+ Paper is a preference the reader opts into, shared with the docs through
+ Starlight's own `starlight-theme` key so one choice covers both surfaces.
+ The mock follows the ground, and the mauve shifts with it — one mauve in
+ view at any time, never two. */
+html,
+html[data-mode="ink"] {
+ /* The ground is near-neutral by test, not by taste. A blue-violet dark at
+ 20% saturation measurably matched the old site's hue, but read as harder
+ — saturated blue at low luminance softens the edges of light text, and
+ the eye has least acuity there. Draining it to 6% won a side-by-side.
+ Colour now lives in the mauve accent, which pops harder against neutral.
+ Contrast: ink 14.7, dim 9.3, faint 5.3 — all above AA. */
+ --bg: #17171a; --panel: #1e1e22; --mass: #26262b;
+ --ink: #eae8ee; --dim: #bdb9c9; --faint: #8e8a98;
+ --line: #26262b; --line2: #35353d;
+ --spot: #cba6f7; --spot-ink: #17171a;
+ --grid: #202024;
+}
+html[data-mode="paper"] {
+ --st-done: #2aa198;
+ --bg: #efece5; --panel: #e7e3da; --mass: #ddd8cc;
+ --ink: #15140f; --dim: #55534a; --faint: #86826f;
+ --line: #e2ded4; --line2: #cbc5b6;
+ --spot: #8839ef; --spot-ink: #fff;
+ --grid: #e4e0d6;
+}
+:root {
+ --run: #5fae74; --wait: #d3a027; --idle: #5a615c; --done: #6f6a86;
+ /* The TUI's own mapping, not a palette invented for the website:
+ src/ui/status.rs state_dot() paints blocked with p.red, working with
+ p.yellow and idle with p.green once seen, and the default palette in
+ src/app/state.rs is Catppuccin Mocha — the same #f38ba8 / #f9e2af /
+ #a6e3a1 that style.css already defines for the plate. Aliasing rather
+ than copying keeps the legend, the plate and the app in step, and lets
+ all three follow the ground when it changes. */
+ --st-blocked: var(--red);
+ --st-working: var(--yellow);
+ --st-idle: var(--green);
+ /* done is p.teal, and style.css has no teal token to alias, so it is set
+ per ground: Catppuccin Mocha teal on ink, Solarized cyan on paper */
+ --st-done: #94e2d5;
+ --disp: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
+ /* Sentences are not code. Mono removes the word-shape cues you read by, so
+ it costs speed in prose and gets worse below ~14px. Inter is already what
+ the docs declare, so using it here makes the two surfaces agree. */
+ --body: "Inter", system-ui, -apple-system, "Segoe UI", Helvetica, sans-serif;
+ --gut: 34px;
+}
+
+/* The plate's chrome takes the page's spot, so there is exactly one mauve
+ in view. Agent identity colours inside the panes (claude coral, opencode
+ purple, codex) stay native. */
+/* The plate's agent states follow the TUI, not the page accent. `done` reads
+ var(--accent) in style.css, and aliasing accent to the page mauve painted a
+ done agent purple — a colour Herdr's sidebar never shows. src/ui/status.rs
+ state_dot() is the authority: blocked red, working filled yellow, done
+ filled teal, idle hollow green, unknown grey. */
+body.site .mock-agent.done > span,
+body.site .mock-agent.done small { color: var(--st-done); }
+
+html[data-palette] {
+ --accent: var(--spot);
+ --term-tab-active-bg: var(--spot);
+ --term-tab-active-text: var(--spot-ink);
+ --term-prompt: var(--spot);
+ --term-blue: var(--spot);
+}
+
+/* The ported plugins page still reads style.css tokens directly, and style.css
+ only knows the 20 mock palettes — so catppuccin's violet #181825 was reaching
+ the publish card, which is why that block stayed tinted after the ground went
+ neutral. Bridge the legacy names onto the neutral scale at the page wrapper.
+ Scoped to this page, not to body.site, because the mock reads --radius-* for
+ its tabs and panes and must keep its own corners. Terminal tokens are left
+ alone on purpose: the plate stays catppuccin. */
+body.site .plugins-page {
+ --bg-elevated: var(--panel);
+ --ink-soft: var(--dim);
+ --muted: var(--faint);
+ --muted-2: var(--faint);
+ --line-strong: var(--line2);
+ --accent-soft: color-mix(in srgb, var(--spot) 8%, transparent);
+ /* the language is hairline cells in a sheet; nothing here is a rounded box */
+ --radius-sm: 0; --radius-md: 0; --radius-lg: 0;
+}
+
+body.site {
+ margin: 0; background: var(--bg); color: var(--ink);
+ font-family: var(--body); font-size: 14.5px; line-height: 1.65;
+ -webkit-font-smoothing: antialiased;
+ background-image:
+ linear-gradient(var(--grid) 1px, transparent 1px),
+ linear-gradient(90deg, var(--grid) 1px, transparent 1px);
+ background-size: 72px 72px;
+ overflow-x: clip;
+ /* exactly one grid module of ground below the sheet */
+ padding-bottom: 72px;
+}
+body.site ::selection { background: var(--spot); color: var(--spot-ink); }
+body.site a { color: inherit; }
+body.site .ram {
+ background: currentColor; display: block;
+ -webkit-mask: url("/assets/ram.svg") center/contain no-repeat;
+ mask: url("/assets/ram.svg") center/contain no-repeat;
+}
+body.site .hd-frame {
+ max-width: 1440px; margin: 0 auto; background: var(--bg); position: relative;
+ border-left: 1px solid var(--line2); border-right: 1px solid var(--line2);
+ /* close the sheet: without this the two vertical rules stopped in mid-air on
+ the last pixel of the document, which read as a crop rather than an end */
+ border-bottom: 1px solid var(--line2);
+}
+
+/* ── nav ── */
+body.site .hd-nav {
+ display: flex; align-items: center; gap: 24px;
+ height: 60px; padding: 0 var(--gut); border-bottom: 1px solid var(--line2);
+}
+body.site .lock { display: flex; align-items: center; gap: 10px; text-decoration: none; }
+body.site .lock .ram { width: 25px; height: 25px; color: var(--ink); }
+body.site .lock b { font-family: var(--disp); font-weight: 900; font-size: 20px; letter-spacing: -.045em; color: var(--ink); }
+body.site .hd-nav .sp { flex: 1; }
+body.site .hd-nav a.lnk { color: var(--dim); text-decoration: none; font-size: 11.5px; letter-spacing: .07em; text-transform: uppercase; }
+body.site .hd-nav a.lnk:hover { color: var(--ink); }
+body.site .hd-nav .btn { border: 1px solid var(--line2); padding: 7px 15px; color: var(--ink); text-decoration: none; font-size: 11.5px; letter-spacing: .07em; text-transform: uppercase; }
+body.site .hd-nav .btn:hover { background: var(--spot); color: var(--spot-ink); border-color: var(--spot); }
+/* Square hairline cells, the same box as the mode toggle and the Install
+ button — so the nav reads as one row of cells rather than text plus chrome.
+ The GitHub one widens to carry its count; the count is real, baked at build
+ and refreshed live, so it is proof rather than decoration. */
+body.site .hd-nav .ico {
+ display: inline-flex; align-items: center; justify-content: center;
+ width: 30px; height: 30px; flex: 0 0 auto;
+ border: 1px solid var(--line2); color: var(--dim); text-decoration: none;
+}
+body.site .hd-nav .ico.wide { width: auto; gap: 8px; padding: 0 10px; }
+body.site .hd-nav .ico svg { width: 14px; height: 14px; fill: currentColor; }
+body.site .hd-nav .ico b {
+ font-family: var(--mono); font-weight: 400; font-size: 11.5px;
+ letter-spacing: .04em; font-variant-numeric: tabular-nums;
+}
+/* an empty count must not leave a lopsided cell */
+body.site .hd-nav .ico.wide:has(b:empty) { width: 30px; gap: 0; padding: 0; }
+body.site .hd-nav .ico:hover { color: var(--spot); border-color: var(--spot); }
+
+/* the menu cell is the same box as the icons and Install, so the nav stays one
+ row of cells; it only exists below the nav's own breakpoint */
+body.site .hd-nav .menu {
+ display: none; align-items: center; justify-content: center; gap: 4px;
+ flex-direction: column; width: 30px; height: 30px; flex: 0 0 auto; padding: 0;
+ border: 1px solid var(--line2); background: none; color: var(--dim); cursor: pointer;
+}
+body.site .hd-nav .menu span { display: block; width: 13px; height: 1px; background: currentColor; }
+body.site .hd-nav .menu[aria-expanded="true"] { color: var(--spot); border-color: var(--spot); }
+
+/* The sheet is a continuation of the nav rather than a modal: same hairline
+ rows, same cell language, dropped under the bar it belongs to. */
+body.site .sheet { display: none; border-bottom: 1px solid var(--line2); }
+body.site .sheet a {
+ display: block; padding: 15px var(--gut); text-decoration: none; color: var(--ink);
+ font-size: 12px; letter-spacing: .07em; text-transform: uppercase;
+ border-bottom: 1px solid var(--line);
+}
+body.site .sheet a:last-of-type { color: var(--dim); }
+body.site .sheet a:active { background: color-mix(in srgb, var(--spot) 8%, transparent); }
+body.site .sheet-ground {
+ display: flex; align-items: center; justify-content: space-between; gap: 16px;
+ padding: 13px var(--gut);
+}
+body.site .sheet-ground > span {
+ color: var(--faint); font-size: 10.5px; letter-spacing: .2em; text-transform: uppercase;
+}
+
+body.site .mode { display: flex; border: 1px solid var(--line2); }
+body.site .mode button {
+ background: none; border: 0; color: var(--faint); cursor: pointer;
+ font-family: var(--mono); font-size: 10px; letter-spacing: .16em;
+ text-transform: uppercase; padding: 7px 10px;
+}
+body.site .mode button:hover { color: var(--ink); }
+body.site .mode button[aria-pressed="true"] { background: var(--ink); color: var(--bg); }
+
+/* ── hero ──
+ Single column. The ram is mass bleeding off the right edge, not an
+ object sitting in a second column with nothing to do. */
+body.site .hd-hero { position: relative; overflow: hidden; padding: 68px var(--gut) 56px; }
+body.site .ghost {
+ position: absolute; right: -14%; top: 50%; transform: translateY(-50%);
+ width: min(880px, 62vw); height: min(880px, 62vw);
+ color: var(--mass); z-index: 0; pointer-events: none;
+}
+body.site .hd-hero .in { position: relative; z-index: 1; }
+body.site .hd-eyebrow { display: flex; align-items: center; gap: 14px; color: var(--spot); font-size: 11px; letter-spacing: .3em; text-transform: uppercase; }
+body.site .hd-eyebrow s { width: 42px; height: 1px; background: var(--spot); text-decoration: none; }
+body.site h1 {
+ font-family: var(--disp); font-weight: 900; letter-spacing: -.055em; line-height: .88;
+ /* wide enough that each sentence takes one line and the type reaches
+ across the frame — a narrow measure is what left the right side empty */
+ font-size: clamp(38px, 6.6vw, 132px); margin: 24px 0 0; max-width: 20ch; color: var(--ink);
+}
+body.site h1 em { font-style: normal; color: var(--spot); }
+body.site .lede { color: var(--dim); font-size: 15px; line-height: 1.75; max-width: 50ch; margin: 32px 0 0; }
+body.site .lede b { color: var(--ink); font-weight: 400; }
+
+/* one command, one link — every other install path lives on /install/ */
+body.site .go { display: flex; align-items: center; gap: 22px; margin-top: 38px; flex-wrap: wrap; }
+body.site .hd-cmd {
+ display: inline-flex; align-items: center; border: 1px solid var(--line2);
+ background: color-mix(in srgb, var(--bg) 80%, transparent); backdrop-filter: blur(6px);
+}
+body.site .hd-cmd code { padding: 14px 19px; font-size: 13.5px; font-family: var(--mono); }
+body.site .hd-cmd code b { color: var(--spot); font-weight: 400; margin-right: 11px; }
+body.site .hd-cmd button {
+ padding: 14px 16px; border: 0; border-left: 1px solid var(--line2); background: none; cursor: pointer;
+ color: var(--faint); font-family: var(--mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
+}
+body.site .hd-cmd button:hover { color: var(--ink); }
+body.site .meta { margin-top: 20px; color: var(--faint); font-size: 11.5px; letter-spacing: .04em; }
+body.site .meta a { color: var(--spot); text-decoration: none; }
+body.site .meta a:hover { text-decoration: underline; }
+
+/* ── numbers ──
+ Every value is real: baked at build from the R2 snapshots, refreshed live
+ and counted up if it moved since the last deploy. */
+body.site .strip { display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid var(--line2); border-bottom: 1px solid var(--line2); }
+body.site .stat {
+ position: relative; display: block; overflow: hidden; text-decoration: none;
+ padding: 24px var(--gut) 26px; border-right: 1px solid var(--line);
+}
+body.site .stat:last-child { border-right: 0; }
+body.site .stat:hover { background: color-mix(in srgb, var(--spot) 5%, transparent); }
+body.site .stat b {
+ position: relative; z-index: 1; display: block;
+ font-family: var(--disp); font-weight: 900; font-size: clamp(26px, 3vw, 40px);
+ letter-spacing: -.045em; line-height: 1; color: var(--ink);
+ font-variant-numeric: tabular-nums;
+}
+body.site .stat span {
+ position: relative; z-index: 1;
+ display: flex; align-items: center; gap: 9px; margin-top: 11px;
+ color: var(--faint); font-size: 10px; letter-spacing: .2em; text-transform: uppercase;
+}
+body.site .stat:hover span { color: var(--dim); }
+body.site .stat .ic {
+ width: 14px; height: 14px; flex: 0 0 14px;
+ fill: currentColor; stroke: currentColor; color: var(--spot); opacity: .85;
+}
+body.site .stat .ic [fill="none"] { fill: none; }
+body.site .stat i.live {
+ width: 6px; height: 6px; flex: 0 0 6px; background: var(--run);
+ animation: site-pulse 2.2s ease-in-out infinite;
+}
+@keyframes site-pulse { 0%,100% { opacity: .25 } 50% { opacity: 1 } }
+
+
+@media (prefers-reduced-motion: reduce) {
+ body.site .stat i.live { animation: none; }
+}
+
+/* ── FIG 1 · the plate ──
+ Inset with paper around it and a hairline border, so it reads as a plate
+ resting on the sheet rather than a hole punched through it. */
+body.site .fig { padding: 40px var(--gut) 46px; border-bottom: 1px solid var(--line2); }
+body.site .fighd {
+ display: flex; align-items: baseline; gap: 16px; padding: 0 0 11px;
+ color: var(--faint); font-size: 10.5px; letter-spacing: .2em; text-transform: uppercase;
+}
+body.site .fighd i { font-style: normal; color: var(--spot); letter-spacing: .1em; }
+body.site .fighd .r { margin-left: auto; text-align: right; }
+body.site .figcap {
+ margin: 13px 0 0; color: var(--faint); font-size: 11.5px; line-height: 1.7; max-width: 74ch;
+}
+body.site .figcap b { color: var(--dim); font-weight: 400; }
+body.site .plate {
+ border: 1px solid var(--line2); overflow: hidden;
+ aspect-ratio: 1438 / 620; background: var(--term-bg);
+}
+
+body.site .plate .hero-product-shot { height: 100%; margin: 0; padding: 0; }
+body.site .plate .hero-product-shot figcaption { display: none; }
+body.site .plate .herdr-ui-mock {
+ max-width: none; width: 100%; height: 100%; aspect-ratio: auto;
+ border: 0; border-radius: 0; box-shadow: none;
+ font-size: clamp(.6rem, .92cqw, .95rem);
+}
+/* .mock-main only sets min-width: 0, so as a grid item it grows to its
+ content height and the plate never clamps the pane */
+body.site .plate .mock-main { min-height: 0; }
+body.site .plate .mock-panes { grid-template-rows: minmax(0, 1fr); }
+
+/* ── router rows: signposts, not pillars ──
+ Stacked so each gets room and the destination is the heaviest thing in
+ the row — these are for going somewhere, not for persuading. */
+body.site .caps { border-bottom: 1px solid var(--line2); }
+/* The whole row is the link, so the destination button is gone and the marks
+ move to the right — which is also where the frame had a half-empty column
+ doing nothing. The arrow is the affordance and it is always visible, since a
+ row that only reveals it on hover gives a reader nothing to go on. */
+/* Three columns: index, text at its measure, evidence. The row used to be text
+ plus a lot of nothing, and making the whole row a hover target drew a box
+ around the nothing. Every row now ends in something real — the session the
+ plate above shows, the agent states the copy names, the commands agents run,
+ the agents themselves — so the width is used rather than merely spanned. */
+body.site .caps > .cap {
+ display: grid; grid-template-columns: 130px minmax(0, 58ch) minmax(0, 1fr);
+ gap: 0 40px; align-items: start;
+ padding: 26px var(--gut); border-bottom: 1px solid var(--line);
+ text-decoration: none; cursor: pointer;
+}
+body.site .caps > .cap:last-child { border-bottom: 0; }
+body.site .caps > .cap:hover { background: color-mix(in srgb, var(--spot) 4%, transparent); }
+body.site .caps > .cap:hover h3 { color: var(--spot); }
+/* .arw, not .go — .go is already the hero's button group and carries a
+ 38px top margin that pushed the arrow to the bottom of the column.
+ It rides the headline rather than sitting in the mark column: with the
+ marks moved left, an arrow down there would point away from the text and
+ away from the destination. On the headline it sits where the eye lands. */
+/* Inline, not inline-block: an atomic inline carries a wrap opportunity before
+ it whatever the whitespace does, so at narrow widths the arrow dropped to a
+ line of its own under the heading. As plain inline text the no-break space in
+ the markup binds it to the last word. The hover nudge moves the margin for
+ the same reason — transforms don't apply to a non-replaced inline. */
+body.site .caps .arw {
+ margin-left: 4px; vertical-align: 2px;
+ font-family: var(--body); font-weight: 400; font-size: 15px; line-height: 1;
+ color: var(--faint); transition: color .12s ease, margin-left .12s ease;
+}
+body.site .caps > .cap:hover .arw { color: var(--spot); margin-left: 8px; }
+/* The marks lead the row. On the right they sat past the end of a 62ch
+ measure and read as debris pinned to the frame edge; on the left they are a
+ margin the text hangs off, and the row's ragged right edge is just how
+ columns end. The index is set large for mass but kept low in contrast — it
+ is texture and position, not a second headline competing with the h3. */
+body.site .caps .n {
+ display: flex; flex-direction: column; gap: 13px; align-items: flex-start;
+ align-self: start; line-height: 1;
+}
+body.site .caps .n > span:first-child {
+ font-family: var(--disp); font-weight: 900; font-size: 44px;
+ letter-spacing: -.05em; color: var(--mass); transition: color .12s ease;
+}
+body.site .caps > .cap:hover .n > span:first-child { color: var(--line2); }
+
+/* One frame for all four, at one width, flush to the right gutter — so the
+ evidence reads as the same kind of object in every row and the column has a
+ straight edge instead of a ragged one. The frame is the terminal's, moved up
+ from .tape so the typing line no longer owns a border the others lack. */
+body.site .caps .ev {
+ align-self: center; justify-self: end;
+ width: 100%; max-width: 520px;
+ border: 1px solid var(--line2); background: var(--panel);
+ padding: 13px 15px;
+ font-family: var(--mono); font-size: 11.5px; color: var(--faint);
+}
+body.site .caps .ev-row { display: flex; align-items: center; gap: 14px; padding: 6px 0; }
+body.site .caps .ev-row > span { color: var(--dim); }
+body.site .caps .ev-row b {
+ margin-left: auto; font-weight: 400;
+ display: inline-flex; align-items: center; gap: 9px;
+}
+body.site .caps .ev-row i { display: block; width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
+body.site .caps .ev-note { margin-top: 9px; line-height: 1.7; }
+
+/* ── 05, the tease ──
+ Inert by design: no link, no pointer, no hover reveal, no arrow. It reads
+ the same to everyone on every device, which is the only version of this
+ that a phone gets too. The blocks are set in mono because JetBrains Mono
+ actually carries U+2580–259F; in the display face they would fall back to
+ whatever the system happens to have. */
+body.site .caps .cap-soon { cursor: default; }
+body.site .caps .cap-soon .mask { font-family: var(--mono); color: var(--line2); letter-spacing: -.02em; }
+body.site .caps .cap-soon .tape { color: var(--faint); }
+/* The line is fully redacted, so there is nothing being typed. A caret would
+ imply a live prompt and put the one moving thing on the page next to the row
+ that is deliberately saying nothing. */
+body.site .caps .cap-soon .tape > span { border-right: 0; animation: none; }
+/* available to a screen reader, never on screen */
+body.site .sr {
+ position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
+ overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
+}
+
+/* green live dot: "this is up right now" */
+body.site .caps .beat {
+ display: block; width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto;
+ background: var(--run); animation: site-pulse 2.2s ease-in-out infinite;
+}
+/* blocked, working, idle — traffic-light order, and all three sit still: they
+ are a legend, and a legend that moves reads as an alarm. Only the heartbeat
+ in the row above animates. */
+body.site .caps .ev-row .b { background: var(--st-blocked); }
+body.site .caps .ev-row .w { background: var(--st-working); }
+/* hollow, because state_dot() draws idle as ○ and every other state as ● —
+ the fill is part of the signal, not decoration */
+body.site .caps .ev-row .i { background: none; box-shadow: inset 0 0 0 2px var(--st-idle); }
+
+/* the typing line. The frame lives on .ev now, so this is just the line —
+ still nowrap and clipped, so a long command can never reflow the box. */
+body.site .caps .tape {
+ display: flex; align-items: baseline; gap: 9px; margin: 0;
+ color: var(--dim); min-height: 19px; overflow: hidden; white-space: nowrap;
+}
+body.site .caps .tape b { color: var(--spot); font-weight: 400; }
+/* The caret is the span's own right border, not a sibling box. As a sibling it
+ sat at the shrunk span's edge while the text overflowed past it, which drew
+ the caret on top of the words — and a block sibling can't sit on the text
+ baseline. A border rides the line box, so it lands after the last character
+ at the right height by construction. */
+/* direct child only: row 05's line wraps a nested redaction block, and a
+ descendant selector gave that block a caret of its own mid-line */
+body.site .caps .tape > span {
+ flex: 0 1 auto; min-width: 0;
+ border-right: 7px solid var(--dim);
+ animation: site-caret 1.05s steps(1) infinite;
+}
+@keyframes site-caret { 0%, 49% { border-color: var(--dim) } 50%, 100% { border-color: transparent } }
+
+/* Masked, not inlined: the source files carry their own brand fills, and one
+ of them is white — which would vanish on paper. Silhouettes in one colour
+ also stop the row turning into a logo salad. */
+/* two columns so eight entries stay a compact block rather than a tall list
+ that would tower over the other three evidence boxes */
+body.site .caps .herd {
+ display: grid; grid-template-columns: 1fr 1fr; gap: 11px 24px; margin: 0;
+}
+body.site .caps .herd span { display: flex; align-items: center; gap: 11px; color: var(--dim); }
+body.site .caps .herd i {
+ width: 15px; height: 15px; flex: 0 0 auto; background: var(--faint);
+ -webkit-mask: var(--m) center/contain no-repeat;
+ mask: var(--m) center/contain no-repeat;
+}
+body.site .caps > .cap:hover .herd i { background: var(--dim); }
+
+@media (prefers-reduced-motion: reduce) {
+ body.site .caps .beat,
+ body.site .caps .tape > span { animation: none; }
+}
+body.site .caps h3 { margin: 0; font-family: var(--disp); font-weight: 800; letter-spacing: -.03em; font-size: 21px; color: var(--ink); }
+body.site .caps p { margin: 9px 0 0; color: var(--dim); font-size: 13px; line-height: 1.7; max-width: 62ch; }
+body.site .caps h3 { transition: color .12s ease; }
+
+/* ── page head: shared by blog and any other framed page ── */
+body.site .ph { padding: 68px var(--gut) 44px; border-bottom: 1px solid var(--line2); }
+/* a wide frame left the right half of the head empty — give it a slot with
+ something real in it rather than widening the measure past readability */
+body.site .ph-split {
+ display: grid; grid-template-columns: minmax(0, 1fr) auto;
+ gap: 60px; align-items: end;
+}
+body.site .ph-side { text-align: right; }
+body.site .ph-side b {
+ display: block; font-family: var(--disp); font-weight: 900;
+ font-size: clamp(40px, 4.6vw, 68px); letter-spacing: -.05em; line-height: 1; color: var(--ink);
+}
+body.site .ph-side span {
+ display: block; margin-top: 10px;
+ color: var(--faint); font-size: 10px; letter-spacing: .2em; text-transform: uppercase;
+}
+body.site .ph-side a { display: inline-block; margin-top: 18px; color: var(--spot); text-decoration: none; font-size: 12px; }
+body.site .ph-side a:hover { text-decoration: underline; }
+body.site .ph h1 {
+ font-family: var(--disp); font-weight: 900; letter-spacing: -.05em; line-height: .92;
+ font-size: clamp(34px, 5vw, 72px); margin: 22px 0 0; max-width: 18ch; color: var(--ink);
+}
+body.site .ph p { margin: 22px 0 0; color: var(--dim); font-size: 14.5px; line-height: 1.75; max-width: 56ch; }
+
+body.site .ph code {
+ color: var(--ink); background: var(--panel);
+ border: 1px solid var(--line2); padding: 1px 6px; font-size: 12px;
+ /* a chip split across two lines reads as a rendering fault, not a wrap */
+ white-space: nowrap;
+}
+body.site .ph-actions { display: flex; gap: 12px; margin-top: 30px; flex-wrap: wrap; }
+body.site .btn-solid, body.site .btn-ghost {
+ text-decoration: none; padding: 12px 18px; font-size: 11.5px;
+ letter-spacing: .07em; text-transform: uppercase; border: 1px solid var(--line2);
+}
+body.site .btn-solid { background: var(--spot); border-color: var(--spot); color: var(--spot-ink); }
+body.site .btn-ghost { color: var(--ink); }
+body.site .btn-ghost:hover { background: var(--spot); border-color: var(--spot); color: var(--spot-ink); }
+
+/* The ported pages still carry style.css's .button classes on the publish
+ links and the load-more control. Given the site language here rather than
+ swapped in the markup, so all three stay in step. The solid one rendered as
+ an empty light slab because `body.site a { color: inherit }` outranks
+ .button-primary's own `color`, so the label took the card's near-white. */
+body.site .button {
+ min-height: 0; padding: 12px 18px; border-radius: 0;
+ border: 1px solid var(--line2); font-family: var(--mono); font-weight: 400;
+ font-size: 11.5px; letter-spacing: .07em; text-transform: uppercase;
+}
+body.site .button-primary { background: var(--spot); border-color: var(--spot); color: var(--spot-ink); }
+body.site .button-primary:hover { background: var(--ink); border-color: var(--ink); color: var(--bg); }
+body.site .button-secondary { background: none; color: var(--ink); }
+body.site .button-secondary:hover { background: var(--spot); border-color: var(--spot); color: var(--spot-ink); }
+
+/* ── plugins ──
+ The markup is the original page's; only the language is restyled, so the
+ live fetch, search and sort behaviour is untouched. ── */
+/* not flex: a wrapping flex row put the status dot on a line of its own */
+body.site .plugins-trust {
+ display: block; margin: 20px 0 0;
+ color: var(--faint); font-size: 12px; line-height: 1.7; max-width: 78ch;
+}
+body.site .plugins-trust .status-dot {
+ display: inline-block; width: 7px; height: 7px; border-radius: 50%;
+ margin-right: 9px; vertical-align: 1px; background: var(--idle);
+}
+body.site .plugins-trust a { color: var(--spot); text-decoration: none; }
+body.site .plugins-controls {
+ display: flex; gap: 16px; align-items: center; flex-wrap: wrap;
+ padding: 20px var(--gut); border-bottom: 1px solid var(--line2);
+}
+body.site .plugins-search-wrap { position: relative; flex: 1; min-width: 240px; }
+body.site .plugins-search-icon {
+ position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
+ color: var(--spot); font-size: 13px;
+}
+body.site .plugins-search {
+ width: 100%; background: var(--panel); border: 1px solid var(--line2); border-radius: 0;
+ color: var(--ink); font-family: var(--mono); font-size: 13px; padding: 12px 14px 12px 34px;
+}
+body.site .plugins-search::placeholder { color: var(--faint); }
+body.site .plugins-search:focus { outline: 0; border-color: var(--spot); }
+body.site .plugins-sort-wrap { display: flex; align-items: center; gap: 10px; }
+body.site .plugins-sort-label { color: var(--faint); font-size: 10px; letter-spacing: .2em; text-transform: uppercase; }
+body.site .plugins-sort {
+ background: var(--panel); border: 1px solid var(--line2); border-radius: 0;
+ color: var(--ink); font-family: var(--mono); font-size: 12.5px;
+ padding: 11px 34px 11px 13px; cursor: pointer;
+ appearance: none; -webkit-appearance: none;
+ background-image: linear-gradient(45deg, transparent 50%, var(--faint) 50%),
+ linear-gradient(135deg, var(--faint) 50%, transparent 50%);
+ background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%;
+ background-size: 5px 5px, 5px 5px;
+ background-repeat: no-repeat;
+}
+body.site .plugins-sort:focus { outline: 0; border-color: var(--spot); }
+/* the count sits flush against the frame otherwise */
+body.site .plugins-status { margin-left: auto; padding: 0; white-space: nowrap; }
+body.site .plugins-metabar { border-bottom: 1px solid var(--line); }
+body.site .plugins-meta,
+body.site .plugins-status {
+ margin: 0; padding: 13px var(--gut); color: var(--faint); font-size: 10.5px;
+ letter-spacing: .18em; text-transform: uppercase;
+}
+body.site .plugins-status { border-top: 1px solid var(--line); }
+/* the old page wraps every block in .section, which brings its own vertical
+ padding, a top border and a horizontal inset — so blocks drifted further
+ right the deeper they were nested. Neutralise it and let each block own
+ its gutter, aligned with the page head. */
+body.site .plugins-page .section {
+ padding: 0; border-top: 0; max-width: none; margin: 0;
+}
+
+body.site .plugins-grid {
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+ gap: 14px; padding: 26px var(--gut) 34px;
+}
+/* cards are hairline cells in a sheet, not filled boxes floating on it */
+body.site .plugin-card {
+ display: flex; flex-direction: column; min-width: 0;
+ padding: 20px 22px 22px; text-decoration: none;
+ background: none; border: 1px solid var(--line2); border-radius: 0;
+ text-transform: none; letter-spacing: normal;
+}
+body.site .plugin-card:hover {
+ background: color-mix(in srgb, var(--spot) 5%, transparent); border-color: var(--spot);
+}
+body.site .plugin-card-head { display: flex; align-items: baseline; gap: 12px; }
+body.site .plugin-title { min-width: 0; font-size: 15px; letter-spacing: -.01em; }
+body.site .plugin-owner { color: var(--faint); }
+body.site .plugin-name { color: var(--ink); font-weight: 500; }
+body.site .plugin-card:hover .plugin-name { color: var(--spot); }
+body.site .plugin-go { margin-left: auto; color: var(--faint); font-size: 14px; }
+body.site .plugin-card:hover .plugin-go { color: var(--spot); }
+/* clamp to two lines so every cell has the same shape — the feed's own
+ ellipsis truncated at a character count, which made the rows ragged */
+body.site .plugin-desc {
+ margin: 11px 0 0; color: var(--dim); font-size: 12.5px; line-height: 1.65;
+ text-transform: none; letter-spacing: normal;
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
+ overflow: hidden; min-height: calc(2 * 1.65em);
+}
+body.site .plugin-meta-row {
+ display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
+ margin-top: auto; padding-top: 18px;
+ color: var(--faint); font-size: 10.5px; letter-spacing: .14em; text-transform: uppercase;
+}
+body.site .plugin-stat { display: flex; align-items: center; gap: 7px; }
+body.site .plugin-star { color: var(--spot); }
+body.site .plugin-lang-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
+body.site .plugin-updated { margin-left: auto; }
+/* .plugins-more is the button, not a wrapper — given section padding it
+ inflated into a floating slab. It's the foot of a long list, so make it the
+ full-width row a list foot should be rather than a box parked in the middle. */
+body.site .plugins-more {
+ display: block; width: 100%; margin: 0; padding: 22px var(--gut);
+ border: 0; border-top: 1px solid var(--line2); background: none;
+ color: var(--dim); text-align: center; cursor: pointer;
+}
+body.site .plugins-more:hover {
+ background: color-mix(in srgb, var(--spot) 5%, transparent); color: var(--spot);
+}
+/* the rule above outranks the page's own .plugins-more[hidden] guard, so the
+ exhausted button stayed on screen; restate the guard at this weight */
+body.site .plugins-more[hidden] { display: none; }
+body.site .plugins-publish { padding: 40px var(--gut) 48px; border-bottom: 1px solid var(--line2); }
+body.site .plugins-publish-card { border: 1px solid var(--line2); padding: 26px 28px; }
+/* the card's heading is an h3, so the h2 rule missed it and the page's own
+ mono declaration stood — the one section heading not in Archivo */
+body.site .plugins-publish h2,
+body.site .plugins-publish h3 {
+ margin: 0; font-family: var(--disp); font-weight: 900; letter-spacing: -.035em;
+ font-size: 26px; color: var(--ink);
+}
+body.site .plugins-publish p { margin: 12px 0 0; color: var(--dim); font-size: 13px; line-height: 1.75; max-width: 70ch; }
+body.site .plugins-publish-actions { display: flex; gap: 12px; margin-top: 22px; flex-wrap: wrap; }
+
+/* ── blog index ── */
+body.site .posts { border-bottom: 1px solid var(--line2); }
+body.site .posts a {
+ display: grid; grid-template-columns: 132px minmax(0, 1fr) auto;
+ gap: 0 30px; align-items: baseline; text-decoration: none;
+ padding: 24px var(--gut); border-bottom: 1px solid var(--line);
+}
+body.site .posts a:last-child { border-bottom: 0; }
+body.site .posts a:hover { background: color-mix(in srgb, var(--spot) 5%, transparent); }
+body.site .posts time { color: var(--faint); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; }
+body.site .posts h2 {
+ margin: 0; font-family: var(--disp); font-weight: 800; letter-spacing: -.03em;
+ font-size: 21px; color: var(--ink);
+}
+body.site .posts p { margin: 8px 0 0; color: var(--dim); font-size: 12.5px; line-height: 1.7; max-width: 74ch; }
+body.site .posts .go-arrow { color: var(--faint); font-size: 15px; }
+body.site .posts a:hover .go-arrow { color: var(--spot); }
+
+/* ── article ── */
+/* The measure column is capped at the body's own 76ch. Left as 1fr it ate every
+ spare pixel and parked the sections list against the far edge, with the gap
+ between them growing without limit on a wide display. */
+body.site .article {
+ display: grid; grid-template-columns: minmax(0, 76ch) 224px; gap: 60px;
+ justify-content: center;
+ padding: 54px var(--gut) 76px; border-bottom: 1px solid var(--line2); align-items: start;
+}
+/* No sidebar means no second column, and the measure centres rather than
+ hugging the left gutter with the whole frame empty beside it. Elsewhere on
+ the site a short measure sits left because something else occupies the
+ right; here nothing does. */
+body.site .article-solo { grid-template-columns: minmax(0, 1fr); }
+body.site .article-solo .article-body { margin-inline: auto; }
+body.site .article-body { min-width: 0; max-width: 76ch; }
+body.site .article-meta {
+ display: flex; gap: 16px; align-items: baseline;
+ color: var(--faint); font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
+}
+body.site .article-meta a { color: var(--spot); text-decoration: none; }
+body.site .article h1 {
+ font-family: var(--disp); font-weight: 900; letter-spacing: -.045em; line-height: .95;
+ font-size: clamp(30px, 3.6vw, 52px); margin: 18px 0 0; color: var(--ink);
+}
+body.site .article-lede { margin: 20px 0 0; color: var(--dim); font-size: 15px; line-height: 1.75; }
+body.site .article-rule { margin: 34px 0 0; border: 0; border-top: 1px solid var(--line2); }
+
+/* prose: mono body, display headings, hairlines instead of boxes */
+body.site .prose { margin-top: 34px; color: var(--dim); font-size: 13.5px; line-height: 1.85; }
+body.site .prose > * + * { margin-top: 20px; }
+body.site .prose h2 {
+ font-family: var(--disp); font-weight: 800; letter-spacing: -.03em; font-size: 25px;
+ color: var(--ink); margin-top: 46px; padding-top: 18px; border-top: 1px solid var(--line);
+}
+body.site .prose h3 { font-family: var(--disp); font-weight: 700; font-size: 18px; color: var(--ink); margin-top: 32px; }
+body.site .prose a { color: var(--spot); text-underline-offset: 3px; }
+body.site .prose strong { color: var(--ink); font-weight: 500; }
+body.site .prose code {
+ color: var(--ink); background: var(--panel);
+ border: 1px solid var(--line2); padding: 1px 6px; font-size: 12px;
+}
+body.site .prose pre {
+ background: var(--panel); border: 1px solid var(--line2);
+ padding: 16px 18px; overflow-x: auto; font-size: 12.5px; line-height: 1.7;
+}
+body.site .prose pre code { background: none; border: 0; padding: 0; font-size: inherit; }
+body.site .prose ul, body.site .prose ol { padding-left: 22px; }
+body.site .prose li + li { margin-top: 8px; }
+body.site .prose blockquote {
+ border-left: 2px solid var(--spot); padding-left: 18px; margin-left: 0; color: var(--faint);
+}
+body.site .prose img { max-width: 100%; border: 1px solid var(--line2); }
+body.site .prose table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
+body.site .prose th, body.site .prose td { text-align: left; padding: 9px 12px; border: 1px solid var(--line); }
+body.site .prose th { color: var(--ink); font-weight: 500; }
+
+/* ── post figures ──
+ Diagrams are built from the site's own tokens rather than exported as PNGs.
+ They switch with ink and paper, stay sharp at any density, and a diagram of
+ the sidebar stops being a lie the next time the sidebar changes. */
+body.site .pf { margin: 34px 0 0; }
+body.site .pf-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
+body.site .pf-panel { border: 1px solid var(--line2); background: var(--panel); min-width: 0; }
+body.site .pf-label {
+ display: flex; justify-content: space-between; gap: 10px;
+ padding: 8px 12px; border-bottom: 1px solid var(--line2);
+ color: var(--faint); font-family: var(--mono); font-size: 10px;
+ letter-spacing: .16em; text-transform: uppercase;
+}
+body.site .pf-label b { color: var(--dim); font-weight: 400; }
+body.site .pf-cap { margin: 12px 0 0; color: var(--faint); font-size: 11.5px; line-height: 1.6; }
+
+/* agent rows */
+body.site .pf-row {
+ display: grid; grid-template-columns: 14px minmax(0, 1fr); gap: 9px; align-items: baseline;
+ padding: 7px 12px; font-family: var(--mono); font-size: 12px;
+}
+body.site .pf-row + .pf-row { border-top: 1px solid var(--line); }
+body.site .pf-row strong { color: var(--ink); font-weight: 400; }
+body.site .pf-row small { display: block; color: var(--faint); font-size: 10.5px; }
+body.site .pf-mark { line-height: 1; }
+body.site .pf-row.working .pf-mark, body.site .pf-row.working small { color: var(--st-working); }
+body.site .pf-row.blocked .pf-mark, body.site .pf-row.blocked small { color: var(--st-blocked); }
+body.site .pf-row.done .pf-mark, body.site .pf-row.done small { color: var(--st-done); }
+body.site .pf-row.idle .pf-mark, body.site .pf-row.idle small { color: var(--st-idle); }
+
+/* The left panel animates because the argument is the motion. Four braille
+ frames at 8fps is what 0.7.5 actually did, per working pane, forever. */
+body.site .pf-spin { position: relative; display: inline-block; width: 1ch; }
+body.site .pf-spin i { position: absolute; left: 0; top: 0; font-style: normal; opacity: 0; animation: pf-frame .5s steps(1, end) infinite; }
+body.site .pf-spin i:nth-child(2) { animation-delay: .125s; }
+body.site .pf-spin i:nth-child(3) { animation-delay: .25s; }
+body.site .pf-spin i:nth-child(4) { animation-delay: .375s; }
+@keyframes pf-frame { 0%, 24.9% { opacity: 1; } 25%, 100% { opacity: 0; } }
+@media (prefers-reduced-motion: reduce) {
+ body.site .pf-spin i { animation: none; }
+ body.site .pf-spin i:first-child { opacity: 1; }
+}
+
+/* flow steps */
+body.site .pf-flow { padding: 14px 12px; font-family: var(--mono); font-size: 11.5px; }
+body.site .pf-step { color: var(--dim); padding: 5px 0; }
+body.site .pf-step b { color: var(--ink); font-weight: 400; }
+body.site .pf-step s { display: block; color: var(--faint); text-decoration: none; font-size: 10.5px; padding-top: 2px; }
+body.site .pf-arrow { color: var(--line2); padding: 1px 0; }
+/* the two outcomes are alternatives, not the next two steps, so they get a
+ fork rather than another length of the same connector */
+body.site .pf-fork em { color: var(--faint); font-style: normal; margin-right: 8px; }
+body.site .pf-fork s { padding-left: 34px; }
+body.site .pf-stop { color: var(--st-done); }
+body.site .pf-cost { color: var(--st-blocked); }
+
+/* bars */
+body.site .pf-bars { padding: 16px 14px; font-family: var(--mono); font-size: 11px; }
+body.site .pf-bar-row + .pf-bar-row { margin-top: 16px; }
+body.site .pf-bar-name { color: var(--dim); }
+body.site .pf-bar { display: grid; grid-template-columns: 46px minmax(0, 1fr) 58px; gap: 8px; align-items: center; margin-top: 5px; }
+body.site .pf-bar span:first-child { color: var(--faint); font-size: 10px; }
+body.site .pf-bar i { display: block; height: 9px; min-width: 1px; }
+body.site .pf-bar em { color: var(--faint); font-style: normal; text-align: right; }
+body.site .pf-bar.old i { background: var(--line2); }
+body.site .pf-bar.new i { background: var(--spot); }
+body.site .pf-bar.new em { color: var(--dim); }
+
+@media (max-width: 560px) {
+ body.site .pf-2 { grid-template-columns: minmax(0, 1fr); }
+ body.site .pf-bar { grid-template-columns: 42px minmax(0, 1fr) 52px; }
+}
+
+/* sticky section index */
+body.site .toc { position: sticky; top: 30px; }
+body.site .toc p { margin: 0 0 12px; color: var(--faint); font-size: 10px; letter-spacing: .2em; text-transform: uppercase; }
+body.site .toc nav { display: flex; flex-direction: column; }
+body.site .toc a {
+ color: var(--dim); text-decoration: none; font-size: 12px; line-height: 1.5;
+ padding: 7px 0 7px 13px; border-left: 1px solid var(--line2);
+}
+body.site .toc a:hover { color: var(--ink); }
+body.site .toc a[aria-current] { color: var(--spot); border-left-color: var(--spot); }
+
+/* ── close ── */
+body.site .end { position: relative; overflow: hidden; padding: 96px var(--gut) 104px; border-bottom: 1px solid var(--line2); }
+/* The bleed is a fraction of the mark, not of the section. As a percentage it
+ resolved against .end's height, so the taller the copy wrapped the further
+ the ram sank — on a phone only its crown cleared the divider. */
+body.site .end .ram {
+ --ram-size: min(560px, 54vw);
+ position: absolute; right: -6%; bottom: calc(var(--ram-size) * -0.22);
+ width: var(--ram-size); height: var(--ram-size); color: var(--mass); z-index: 0;
+}
+body.site .end .in { position: relative; z-index: 1; }
+body.site .end h2 { font-family: var(--disp); font-weight: 900; letter-spacing: -.05em; font-size: clamp(36px, 5.4vw, 70px); line-height: .93; margin: 0; max-width: 13ch; color: var(--ink); }
+body.site .end p { margin: 20px 0 0; color: var(--dim); font-size: 13.5px; max-width: 52ch; line-height: 1.8; }
+
+/* ── footer ──
+ The drafting title block that used to sit here is gone. Three of its five
+ cells were marketing in a spec costume, it was identical on every page so it
+ said nothing after the first view, and its version was hardcoded in two
+ files. The two facts worth keeping now sit in the footer line, with the
+ version read from latest.json at build. */
+body.site .foot { padding: 22px var(--gut); border-top: 1px solid var(--line2); display: flex; gap: 22px; align-items: center; flex-wrap: wrap; color: var(--faint); font-size: 11.5px; }
+body.site .foot a { color: var(--dim); text-decoration: none; }
+body.site .foot a:hover { color: var(--ink); }
+body.site .foot .ram { width: 15px; height: 15px; color: var(--faint); }
+body.site .foot .fact { color: var(--faint); font-variant-numeric: tabular-nums; }
+/* the outbound cluster sits right: site links and provenance on one side, ways
+ to reach the project on the other. Its own gap is tighter than the row's, so
+ two 14px marks read as a pair, not as two more items in the list. */
+body.site .foot .out { margin-left: auto; display: flex; align-items: center; gap: 16px; }
+body.site .foot .ico { display: inline-flex; color: var(--faint); }
+body.site .foot .ico svg { width: 14px; height: 14px; fill: currentColor; }
+body.site .foot .ico:hover { color: var(--ink); }
+
+@media (max-width: 900px) {
+ :root { --gut: 20px; }
+ body.site { background-size: 40px 40px; padding-bottom: 40px; }
+ /* One row can't hold the lockup, the count, Install and three links at
+ 390px — those are 450px of cells in 350px. The links move into the sheet,
+ which also carries what phone width otherwise drops silently. */
+ body.site .hd-nav a.lnk { display: none; }
+ /* the toggle is 103px of the bar and the sheet now carries it. Left in, it
+ overflowed the nav at 430–560px, which widened the document past the
+ viewport and left a dead strip down the right of every section. */
+ body.site .hd-nav .mode { display: none; }
+ body.site .hd-nav .menu { display: inline-flex; }
+ body.site .sheet.open { display: block; }
+ /* two up: the column divider alone left the rows floating together, so the
+ four cells read as one block rather than a grid. The bottom row keeps its
+ edge from the strip's own border. */
+ body.site .strip { grid-template-columns: 1fr 1fr; }
+ body.site .strip .stat { border-bottom: 1px solid var(--line); }
+ body.site .strip .stat:nth-last-child(-n + 2) { border-bottom: 0; }
+ body.site .strip .stat:nth-child(2n) { border-right: 0; }
+ /* the evidence drops under the text rather than competing for width */
+ body.site .caps > .cap { grid-template-columns: 54px minmax(0, 1fr); gap: 0 18px; }
+ body.site .caps .n > span:first-child { font-size: 30px; }
+ /* stacked, the card butted straight into the last line of the paragraph —
+ the 20px was inside its border, so it padded the card rather than
+ separating it. Margin instead, and the interior goes back to even. */
+ body.site .caps .ev { grid-column: 2; margin-top: 20px; }
+ body.site .fig { padding: 26px var(--gut) 30px; }
+ body.site .plate { aspect-ratio: auto; }
+ body.site .hd-hero { padding: 60px var(--gut) 52px; }
+ /* 224px of sections beside a phone measure left the title breaking every two
+ words. A sticky jump list is worth least on the screen it costs most, so
+ the post falls back to the same single column a heading-less post uses. */
+ body.site .article { grid-template-columns: minmax(0, 1fr); }
+ body.site .article .toc { display: none; }
+}
+
+/* Below this the nav's cells stop fitting, so they come off in order of how
+ little they're worth on a phone. Measured at the 24px gap: lock 88 + github
+ 88 + x 30 + toggle 103 + install 86 is 515px of cells, and a 480px viewport
+ leaves 440px. X goes first — it's the one thing the footer also carries. */
+@media (max-width: 560px) {
+ body.site .hd-nav { gap: 14px; }
+ body.site .hd-nav .ico[href*="x.com"] { display: none; }
+ /* the hint stops competing for the same line as the title and becomes the
+ line under it — right-aligned, it read as a second ragged column */
+ body.site .fighd { flex-wrap: wrap; gap: 6px 16px; }
+ body.site .fighd .r { margin-left: 0; text-align: left; }
+ /* the close is mostly the watermark's room at this width */
+ body.site .end { padding: 56px var(--gut) 64px; }
+ /* nothing to push against on one column: the cluster hung alone on its own
+ line while the rest of the footer sat left */
+ body.site .foot .out { margin-left: 0; }
+ /* the mark carries no wordmark of its own, so once the row wrapped it read as
+ a stray glyph leading a list rather than as a lockup. The identity is at the
+ top of the page; here it was only costing the row its first cell. */
+ body.site .foot .ram { display: none; }
+ /* the 132px date column left the title about 120px to live in, so every
+ headline broke every two words. The date goes above the entry instead. */
+ body.site .posts a { grid-template-columns: minmax(0, 1fr) auto; gap: 8px 16px; }
+ body.site .posts time { grid-column: 1 / -1; }
+ /* the count column took half the head and squeezed the copy into a
+ three-words-per-line ribbon; stacked, it becomes the head's footer */
+ body.site .ph-split { grid-template-columns: minmax(0, 1fr); gap: 30px; }
+ body.site .ph-side { text-align: left; }
+ body.site .ph-side b { font-size: 44px; }
+}
+
+@media (max-width: 430px) {
+ /* 44 characters at 13.5px ran to a third line with "sh" alone on it. At 12px
+ the only break opportunity — the space after -fsSL — lands mid-line and the
+ URL stays whole. */
+ body.site .hd-cmd code { padding: 12px 14px; font-size: 12px; }
+ body.site .hd-cmd button { padding: 12px 13px; }
+}
+
+/* The narrow end of the range — SE-class phones and folded covers, 320px up.
+ Four cells and three gaps came to 334px against 324px of bar at 364, so the
+ nav overflowed again and the document went wider than the screen. The count
+ is the widest thing that can go and still leave a working link: the mark
+ keeps the destination, and the same number is one screen down in the strip. */
+@media (max-width: 380px) {
+ :root { --gut: 16px; }
+ body.site .hd-nav { gap: 10px; }
+ body.site .hd-nav .ico.wide { width: 30px; gap: 0; padding: 0; }
+ body.site .hd-nav .ico.wide b { display: none; }
+ /* the button's 60px was the difference between two lines and a third with
+ "sh" on it, so it becomes the row under the command instead of the cell
+ beside it — the same shape the plugins list uses for its foot */
+ body.site .hd-cmd { flex-wrap: wrap; }
+ body.site .hd-cmd code { flex: 1 1 100%; }
+ body.site .hd-cmd button {
+ flex: 1 1 100%; text-align: left; padding: 11px 14px;
+ border-left: 0; border-top: 1px solid var(--line2);
+ }
+}
+
+/* The mock scales on its own width — .hero-product-shot is the container.
+ Sidebar left, tabs above the pane: the same layout as the wide plate, just
+ smaller. It used to reflow into stacked strips, which is a shape herdr
+ never renders and which overflowed sideways anyway. Type is the thing that
+ shrinks, so the terminal stays crisp rather than being transform-scaled. */
+@container (max-width: 900px) {
+ body.site .herdr-ui-mock {
+ aspect-ratio: auto; height: 74vh; min-height: 460px;
+ /* the sidebar keeps enough width for a branch name under the workspace */
+ grid-template-columns: minmax(84px, 27%) minmax(0, 1fr);
+ }
+ body.site .mock-space, body.site .mock-agent { padding: 6px 8px; }
+ /* "grouped" is the sort mode; at this width it collides with the heading */
+ body.site .mock-agent-title span:last-child { display: none; }
+ body.site .mock-main { min-height: 0; min-width: 0; }
+ body.site .mock-view, body.site .mock-pane { min-width: 0; }
+ /* splits stack out of view rather than halving an already narrow pane */
+ body.site .mock-panes { grid-template-columns: minmax(0, 1fr) !important; min-width: 0; }
+ body.site .mock-panes .mock-pane:not(.focused) { display: none; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ body.site .strip i.live { animation: none; }
+}
+
+/* ──────────────────────────────────────────────────────────────
+ Typography split — readability first.
+
+ Prose inherits --body from body.site. Everything below is put back
+ into mono because mono is doing a real job there: identifiers and
+ repo paths, commands, uppercase labels, and figures with units.
+ Display headings stay Archivo and are set where they're declared.
+ ────────────────────────────────────────────────────────────── */
+body.site .hd-nav a.lnk,
+body.site .hd-nav .btn,
+body.site .hd-eyebrow,
+body.site .meta,
+body.site .stat span,
+body.site .fighd,
+body.site .caps .n,
+body.site .foot,
+body.site .ph-side span,
+body.site .posts time,
+body.site .article-meta,
+body.site .toc p,
+body.site .plugins-search,
+body.site .plugins-sort,
+body.site .plugins-sort-label,
+body.site .plugins-meta,
+body.site .plugins-status,
+body.site .plugin-title,
+body.site .plugin-meta-row,
+body.site .btn-solid,
+body.site .btn-ghost,
+body.site code,
+body.site pre {
+ font-family: var(--mono);
+}
+
+/* prose sizes: comfortable rather than compact */
+body.site .lede { font-size: 16px; line-height: 1.7; max-width: 56ch; }
+body.site .ph p { font-size: 15.5px; line-height: 1.7; max-width: 62ch; }
+body.site .caps p { font-size: 14.5px; line-height: 1.7; color: var(--dim); }
+body.site .end p { font-size: 15px; line-height: 1.75; }
+body.site .figcap { font-size: 13.5px; line-height: 1.7; }
+body.site .posts p { font-size: 14px; line-height: 1.65; }
+body.site .article-lede { font-size: 16.5px; line-height: 1.7; }
+body.site .prose { font-size: 16px; line-height: 1.75; }
+body.site .prose code, body.site .prose pre { font-size: 13.5px; }
+body.site .toc a { font-family: var(--body); font-size: 13px; line-height: 1.5; }
+body.site .plugins-trust { font-size: 13.5px; line-height: 1.7; }
+body.site .plugin-desc { font-size: 14px; line-height: 1.6; min-height: calc(2 * 1.6em); }
+body.site .plugin-title { font-size: 14.5px; }