A Claude Design handoff (“Krill Walkthrough”) shipped as a standalone HTML
document — its own <head>, a :root{} palette, global html,body/a/button
resets, and unprefixed class selectors. We needed it served at /explore
inside the Chirpy site chrome, maintainable as a drop-in file. Two ways it can
go wrong when embedding such a file into a shared-document theme: (1) its CSS
clobbers the whole site (link colors, body background, box-sizing), and
(2) the nav tab renders the wrong label.
:root, html,body, a, button, *) and
bare class names apply site-wide, not just to the embedded page._includes/sidebar.html derives a tab’s nav
label from the URL slug (tab.url | split: '/'), not the page title —
then upcases it. So for _tabs/explore.md the label is looked up as
tabs.explore in _data/locales/<lang>.yml. Keying the locale entry by the
title (“discover”) instead leaves a dead key and makes the nav fall back to
tab.title by accident._tabs/explore.md (layout: page) that
`It starts with a server and the app of your choice. From there you add nodes — one at a time — and wire them up. By the end of this page you'll understand data points, cron timers, calculations, and the one idea that ties them all together: nodes only react to the things they're told to observe.
Download the Krill server and run it on any machine on your local network. Then open the Krill app on your phone, tablet, laptop, or desktop — it broadcasts on the LAN and the server answers back.
No cloud, no accounts, no port forwarding. Everything stays on your network and everything connects automatically.
A server + the apps connected to it. Servers can also find each other and pair, but that comes later — start with one server.
A data point is the simplest kind of node. It holds a single typed value — a number, a boolean, a string — and remembers it. Anyone connected to the server can read it; anyone with permission can change it.
Right now this is just a number sitting on the server. It doesn't know about anything else, and nothing knows about it. That's fine — every krill graph starts like this.
Every reading, toggle, counter, label and reading in krill is a data point. They are the nouns of the system.
A cron timer is a node that fires on a schedule. "Every 5 seconds", "every weekday at 6am", "the first of every month" — anything you can write as a cron expression.
Drop one into your project alongside the data point. You now have two nodes, sitting next to each other in the same project.
Here's the catch: they don't know about each other. The data point doesn't watch the timer. The timer doesn't write to the data point. They're just two nodes living in the same room.
A scheduler node. Fires an event whenever its schedule says it should — independent of everything else.
Counter.
A calculation node runs a small expression and produces a value. The expression can reference
other nodes — for example, Counter + 1. Calc nodes are how krill does arithmetic, math, and
small bits of logic without needing real code.
We'll also rename myDataPoint to Counter — because that's what it's about to become.
Same situation as before, though: three nodes, zero connections. The calc node knows its formula but hasn't been told when to run. The cron is ticking away, but nothing listens. The counter sits at 0.
A formula node. Re-runs its expression whenever a node it observes changes — and writes the result somewhere you tell it to.
Here's what the cron node looks like in the Krill app — schedule, next fire time, last fire time. It's been running this whole time. Every 5 seconds it emits a "tick" event.
And every 5 seconds… nothing happens. The tick fires into empty air. The counter stays at 0. Other systems would call this a missed signal. In krill, it's just the default state: no node reacts to anything until it explicitly says it wants to.
The cron doesn't push to anyone. It just announces "I ticked". Other nodes have to opt in to hearing about it. This is the observer model — and it's what makes large krill graphs predictable.
Open the calc node and add every5s as one of its sources. That's it. Now the calc
cares about the cron — when the cron fires, the calc wakes up, evaluates its formula, and produces
a result.
Counter + 1 evaluates to 1. The calc has a fresh output value.
But notice what didn't happen: the Counter still shows 0. The calc has produced a result, but nothing is writing it anywhere. The calc can read from the Counter to evaluate its formula — that's just a reference. It doesn't mean the Counter is listening back.
Every node has a list of nodes it's subscribed to. When one of them changes or fires, the node re-evaluates. The list is explicit — you add things to it on purpose.
Last connection. Open Counter and add bumpCounter as one of its sources.
Now the Counter cares when the calc produces a value — and ingests it.
Watch what happens. Every 5 seconds the cron fires. The calc wakes up, reads Counter, evaluates
Counter + 1. The Counter is now listening, so it absorbs the result — and becomes that new value.
Then 5 seconds later the cron fires again, the calc reads the new Counter, adds 1, writes back. The Counter
increments.
That's krill. Three nodes, two wires, a self-incrementing counter — and a mental model that scales to rooms full of sensors, actuators, and logic.
Reading from a node is free. Reacting to a node is opt-in. To make A respond to B, you add B as a source of A. Always. That's the whole rule.
Every krill graph — from a single counter to a greenhouse full of sensors — is built the same way. Add nodes. Tell each one what to listen to. Let the swarm do the rest.
You now know the four words that show up in every krill discussion:
; the design lives in one
self-contained include so future refreshes are a single-file swap. The file
stays named explore.md so the URL is /explore; the visible label is
**Discover** via title: Discover plus the locale entry **explore: Discover**
(keyed by the URL slug, matching how sidebar.html` resolves it).
.krill-walk wrapper: :root vars moved
onto .krill-walk, the html,body/a/button/* resets rescoped to it, all
class selectors prefixed. @keyframes left global (names are unique).<h1 class="dynamic-title"> per tab; hid it for this
page via article:has(.krill-walk) > .dynamic-title{display:none} so the
design’s hero is the single h1.When embedding any standalone HTML/CSS into a shared-document site (Jekyll,
Chirpy, etc.): always scope the entire stylesheet under a unique wrapper class
and rescope global resets (:root, html, body, a, button, *) before
committing. Verify by building and grepping a sibling page for the design’s
wrapper class — it must be absent (this is the no-bleed check). For the nav
label, key the locale tabs entry by the filename/URL slug, not the
display title, and confirm the rendered nav by grepping the built home page.