Strata specs — the Spec Kit

You are an agent authoring a spec, PRD, or design doc for Layers. Build it as a single HTML document on the Strata Spec Kit — never in “anthropic design style”, never with an ad-hoc stylesheet. The kit is ONE self-contained CSS file; the guided way to author is the /spec command.

Authoring with /spec

Open a Claude Code session anywhere in layers/layers and type /spec. The wizard interviews you for the substance (a topic, a draft, or a conversation to summarize), proposes the structure, writes the file to docs/specs/<slug>.html, and publishes immediately — every spec is hosted from its first draft, no exceptions: an always-secret gist plus an htmlpreview link teammates open in one click. The deliverable is the hosted URL, clickable in chat — the wizard never opens or hands back the local file path (not shareable, useless), and the turn isn’t done until the link is in the chat message.

Every update is a version bump: the document’s rail-foot meta (Spec · vN) increments, and the link carries the new gist revision sha as ?v=<sha>. The gist id never changes, so one durable document circulates. Three mechanics in /spec Step 5 exist because each one drew blood:

  • gh gist edit exits 0 and writes nothing. Writes go through gh api -X PATCH gists/<id>, never that command.
  • No link is handed over until the STORED content is read back from the gist API and its version matches what was just written. Exit code 0 is not proof.
  • ?v= is load-bearing, never cosmetic, and every value is single-use. The raw CDN caches per full URL, so fetching a ?v= before the push lands pins that URL to stale content permanently. The value is the revision sha (unique per push), never the document version (collides, gets poisoned).

Living example, authored end to end with the command: https://htmlpreview.github.io/?https://gist.githubusercontent.com/stevendoyledesign/9538904d78e78e3371254ddc9c4a31f8/raw/unified-approvals-walker.html

The kit

  • Stylesheet: hosted at https://strata.layers.com/spec/strata-spec.css, source in the repo at apps/strata/public/spec/strata-spec.css. Specs <link> the hosted file; they never inline it. One line replaces 1062 lines of pasted CSS, and a spec always renders against the current kit. Verified 2026-07-28 through the real delivery path (secret gist → htmlpreview): styling, the embedded fonts, and the theme toggle all work cross-origin.
  • Berkeley Mono (Light/Regular/Medium) ships as data URIs INSIDE that stylesheet, so the fonts arrive with it and cost no second request; the body face is the Inter-first system stack. The Strata mark is inline SVG (the skeleton below carries it) — never an external image.
  • The hosted kit is the only external dependency a spec may have. No CDN scripts, no external fonts, no remote images; author images embed as data URIs. One known limit: a cross-origin stylesheet cannot be read by JS (cssRules throws SecurityError). Rendering is unaffected and neither skeleton script touches it.
  • Tokens are compiled from packages/ui/src/css/theme.css: light-first, dark follows the OS scheme automatically. The rail-foot toggle overrides the OS by stamping data-theme="dark"/"light" on <html>; the kit’s :root[data-theme=…] blocks outrank its prefers-color-scheme block, so the override holds in both directions. Never add colors beyond the kit’s variants; never override tokens in a spec.
  • A rendered reference using every kit section: https://strata.layers.com/spec/demo.html (source: apps/strata/public/spec/demo.html).

Document skeleton

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Short spec title</title>
  <link rel="stylesheet" href="https://strata.layers.com/spec/strata-spec.css" />
  <script>
    // Theme, part 1 of 2: apply a stored choice BEFORE first paint so a reader
    // who overrode the OS scheme never sees a flash of the other side. No
    // stored choice means no attribute, and the OS scheme drives the document.
    // localStorage throws on some file:// origins, hence the try.
    (function () {
      try {
        var t = localStorage.getItem("strata-spec-theme");
        if (t === "dark" || t === "light")
          document.documentElement.setAttribute("data-theme", t);
      } catch (e) {}
    })();
  </script>
</head>
<body>
  <div class="layout">
    <aside>
      <div class="spec-brand">
        <span class="logo-wrap">
        <svg viewBox="0 0 29 27" width="18" height="18" fill="currentColor" aria-hidden="true">
          <path d="M14 23.9648C21.732 23.9648 28 24.6364 28 25.4648C28 26.2933 21.732 26.9648 14 26.9648C6.26801 26.9648 0 26.2933 0 25.4648C0 24.6364 6.26801 23.9648 14 23.9648ZM15 19.9648C22.732 19.9648 29 20.6364 29 21.4648C29 22.2933 22.732 22.9648 15 22.9648C7.26801 22.9648 1 22.2933 1 21.4648C1 20.6364 7.26801 19.9648 15 19.9648ZM12.5107 18.916C6.46702 18.7499 2 18.1642 2 17.4648C2 16.739 6.81129 16.133 13.2031 15.9941L12.5107 18.916ZM13.459 14.9141C7.44155 14.746 3 14.1622 3 13.4648C3 12.741 7.78484 12.1361 14.1504 11.9951L13.459 14.9141ZM14.4062 10.9131C8.41561 10.7431 4 10.1602 4 9.46484C4 8.74307 8.75795 8.1403 15.0967 7.99707L14.4062 10.9131ZM15.3545 6.91113C9.3902 6.73926 5 6.15815 5 5.46484C5 4.74508 9.73158 4.14342 16.0439 3.99805L15.3545 6.91113ZM16.3018 2.91016C10.3643 2.73637 6 2.15611 6 1.46484C6 0.747082 10.7053 0.147497 16.9912 0L16.3018 2.91016Z" />
        </svg>
        </span>
        <span class="wordmark">SPEC</span>
      </div>
      <nav>
        <div class="group">Frame</div>
        <a href="#tldr">TL;DR</a>
        <a href="#context">01 Context</a>
        <div class="group">The work</div>
        <a href="#design">02 Design</a>
        <a href="#phases">03 Phases</a>
      </nav>
      <div class="rail-foot">
        <span class="meta">Spec · v1</span>
        <div class="theme-toggle" role="group" aria-label="Theme">
          <button type="button" data-theme-set="light">Light</button>
          <button type="button" data-theme-set="dark">Dark</button>
        </div>
      </div>
    </aside>
    <main>
      <header class="hero">
        <div class="eyebrow">Product spec · Owner · 2026-07-07</div>
        <h1>The one-line thesis of the spec</h1>
        <p class="deck">Two or three sentences of framing.</p>
        <div class="byline"><span>Status: Draft</span><span>Repo: layers/layers</span></div>
      </header>
      <div class="tldr" id="tldr">
        <div class="label">TL;DR</div>
        <p>One short framing sentence.</p>
        <ul>
          <li><strong>Where we are:</strong> one short line.</li>
          <li><strong>What's next:</strong> one short line.</li>
        </ul>
      </div>
      <section id="context">
        <h2><span class="num">01 · Context</span>Why now</h2>
        <p class="lede">One-paragraph section framing.</p>
        ...
      </section>
    </main>
  </div>
  <script>
    // Scrollspy: the section nearest the top lights its rail link (the
    // docs-site active pill). Reads a.hash (the RESOLVED fragment), never
    // the href attribute: hosted previews (htmlpreview.github.io) rewrite
    // href attributes to absolute URLs, which empties attribute selectors
    // like a[href^="#"]. Self-contained; plain anchors with JS off.
    (function () {
      var links = Array.prototype.slice
        .call(document.querySelectorAll("aside nav a"))
        .filter(function (a) {
          return a.hash && document.getElementById(a.hash.slice(1));
        });
      var targets = links.map(function (a) {
        return document.getElementById(a.hash.slice(1));
      });
      function activate() {
        var y = window.scrollY + 96;
        var current = targets[0];
        for (var i = 0; i < targets.length; i++) {
          if (targets[i].offsetTop <= y) current = targets[i];
        }
        links.forEach(function (a) {
          a.classList.toggle(
            "active",
            !!current && a.hash === "#" + current.id
          );
        });
      }
      window.addEventListener("scroll", activate, { passive: true });
      window.addEventListener("resize", activate);
      activate();
    })();

    // Theme, part 2 of 2: wire the rail-foot toggle. A click stamps
    // data-theme on <html> (the attribute beats the OS media query in both
    // directions) and stores the choice for the next visit. The lit segment
    // reports the LIVE side, whether that came from the OS or an override.
    (function () {
      var root = document.documentElement;
      var mq = window.matchMedia("(prefers-color-scheme: dark)");
      var buttons = Array.prototype.slice.call(
        document.querySelectorAll(".theme-toggle button[data-theme-set]")
      );
      function paint() {
        var attr = root.getAttribute("data-theme");
        var live = attr || (mq.matches ? "dark" : "light");
        buttons.forEach(function (b) {
          b.setAttribute(
            "aria-pressed",
            b.getAttribute("data-theme-set") === live ? "true" : "false"
          );
        });
      }
      buttons.forEach(function (b) {
        b.addEventListener("click", function () {
          var next = b.getAttribute("data-theme-set");
          root.setAttribute("data-theme", next);
          try {
            localStorage.setItem("strata-spec-theme", next);
          } catch (e) {}
          paint();
        });
      });
      if (mq.addEventListener) mq.addEventListener("change", paint);
      // An embedder can stamp data-theme on the root itself, without a click
      // (the /specs preview iframe pushes the site theme in that way), so
      // follow the attribute as well.
      if (window.MutationObserver)
        new MutationObserver(paint).observe(root, {
          attributes: true,
          attributeFilter: ["data-theme"]
        });
      paint();
    })();
  </script>
</body>
</html>

The shell is the strata docs layout: a sticky 240px nav rail with a hairline divider (brand row up top — the Layers mark in its bounded squircle box, the logo is never a bare mark — the grouped section links as 32px pills with a bg-secondary active state, and the Spec · v1 meta pinned in the rail foot beside the light/dark toggle), beside a centered reading column.

Both scripts are part of the skeleton, copied verbatim, and both degrade:

  • Scrollspy drives the active rail pill, and falls back to plain anchors with scripting disabled.
  • The theme toggle ships in two pieces. The head script applies a stored choice before first paint; the body script wires the two segments, stamps data-theme on <html>, and stores the choice under strata-spec-theme. Default is the OS scheme. The lit segment always reports the live side, including when an embedder stamps data-theme on the root itself (an observer on that attribute keeps the segments honest). With scripting off the control is inert and the OS scheme still applies.

The bar (founder ruling 2026-07-15)

A good spec is concise, highly opinionated, and specific. It exists so a human or a future Claude session reads it and understands exactly what is happening and why. No fluff, no ambiguity, no noise.

  • Every section decides something. If a section only collects loose ends, it does not belong in the document.
  • No boilerplate closer sections. “Open questions” and “Explicitly not doing” as trailing sections are banned as defaults. A genuinely undecided item becomes a decision with a stated default (“X, unless Y proves otherwise”) inside the section it affects, or a verification gate in a phase’s acceptance criteria. A rejection earns space only where a reader would otherwise re-propose the alternative, and it sits inline next to the decision it protects.
  • Thin beats padded. Sections stay thin when the substance is thin; never inflate a document to look complete.

The Content Planner spec bar (founder rulings 2026-08-09; the feature was then named Social Loops)

The exemplar every new spec studies first: docs/specs/loops-data-model.html (titled for the feature’s then-name, Social Loops). What it locked:

  • Whiteboard first. A spec about a data model or a flow LEADS with an inline SVG diagram — the one you would draw for a room of engineers who have never seen the feature. Theme-aware by construction: currentColor strokes and text, one <marker> for arrowheads, accent color only on the money path. A TL;DR is not a substitute for a diagram; when the diagram can say it, the TL;DR goes.
  • Golden state only. Entirely future-looking: no today/next duality, no status framing, no legacy names, no “where we are” sections. Progress lives in trackers and code, never in the reference document.
  • Decisions are a table. One label, one one-line rule per row. A decision that needs a paragraph is not decided yet.
  • One unified stores table. Table · holds · one writer · read by — one table beats separate sources/stores/consumers sections.
  • A worked example earns its place. Replay the model on two or three real records (raw → interpreted → clustered) so the abstractions have faces.
  • Wide variant, sanctioned. A table/diagram-heavy spec may carry ONE commented page-level <style> override: content to 1040px, prose to 960px, .tbl-wrap as a capped scroll box with sticky thead th (opaque var(--card) fill, box-shadow standing in for the border sticky cells drop). This is the only sanctioned exemption to the no-<style> audit, and the comment in the file must say so.
  • Living document. A spec that tracks an active design updates in the SAME turn a ruling lands: bump the version, re-push the SAME gist, verify, hand over a fresh ?v= link.

Spec kit sections

“Block” is reserved for the Strata UI tier (a composed multi-component surface like ElleBlock — see components.md). These spec-authoring pieces are sections, a different domain.

The starter vocabulary — reach for these first so recurring structures read identically across specs. When a pattern you invent (see below) recurs across documents, promote it into the kit (edit strata-spec.css + this doc in the same PR).

SectionClassesUse for
Nav railaside + .spec-brand (.logo-wrap + .wordmark), nav with .group headers, .rail-foot + .meta + .theme-toggleThe docs-style rail: brand row up top, GROUPED section links (Frame / The work / …) as pills, the version meta and the light/dark toggle pinned in the foot; long specs carry several groups
Theme toggle.theme-toggle > two button[data-theme-set]Mandatory, in the rail foot of every spec. Markup and script both come verbatim from the skeleton; never restyle it and never hand-roll a second one
TL;DR.tldr + .label, p + ulThe mandatory summary card right after the hero. NEVER a wall paragraph (founder ruling 2026-07-25): one framing sentence of ~15 words, then 3 to 6 bullets, each ONE line anchored by a <strong> lead (“Where we are:”, “What’s next:”). No nested lists, no second paragraph
Section headersection > h2 + span.numNumbered sections (01 · Context)
Callout.callout (+ .note .info .warn .success) + .labelDecisions, constraints, risks, wins
Pull quote.pullquoteOne line worth enlarging
Pill.pill (+ .purple .blue .green .peach)Inline status/category chips
Table.tbl-wrap > tableComparisons, matrices (mono uppercase headers come free)
Anti-list.antilist + .antilist-head, li > .ban + .whyA rejected alternative a reader would re-propose, inline beside the decision it protects (never a trailing junk-drawer section — see “The bar”)
Phase card.phase + .idx, .goal, optional .accept + .deferredRollout phases with acceptance criteria
Concept.concept + .ctagDefinitions of core terms
Checklist.checklistLaunch gates inside a phase — never a trailing “Open questions” section (see “The bar”)
Metrics.metrics > .metric (.value + .label)Headline numbers: targets, baselines, results
Component veneer.spec-btn (.secondary .outline .sm), .spec-badge (.secondary .outline .success), .spec-alert (.title + .desc), .spec-input, .spec-chat > .spec-message (.end) > .spec-bubble (.secondary)Demoing REAL product UI: static, pixel-faithful renderings of the @layers/ui components
Rulehr.ruleHard breaks between arcs
Source note.src-noteFile paths, URLs, commit refs

Beyond the kit — invent freely, on-brand

You have complete freedom to build whatever presents the content best: charts, product wireframes, UI mockups, timelines, diagrams, testimonials, images, comparison visuals, anything HTML/CSS can do. The kit sections are a starter set, not a boundary. What makes an invention on-brand is obeying the style law, not picking from a list:

  • Color: only the kit tokens. Pastels (--brand-purple/blue/green/peach) are flat signal fills and accents; surfaces are --background/--card; lines are --border. Never a new hex, never a gradient, never a shadow, never 3D. Inventions theme automatically ONLY if they use the tokens. Check both sides with the rail-foot toggle.
  • Type: body and data values are the sans at the 14px scale. Every ALL-CAPS label on an invented element uses .mono-label verbatim. Mono never appears on mixed-case text or numbers.
  • Geometry: 8px spacing grid (4px fallback), radii 4/6/8/10px matching the kit, 1px hairlines.
  • Charts: pure HTML/CSS (divs) or inline SVG. Flat pastel fills, hairline axes in --border, .mono-label axis labels, values in sans. No chart libraries (self-containment), no decoration that data doesn’t earn.
  • Product UI: when the spec shows REAL product surfaces (a chat stream, a form, buttons, an alert), use the component veneer classes (.spec-* in the vocabulary table): static, pixel-faithful renderings transcribed verbatim from @layers/ui. Never approximate a real component by hand. The veneer is a manual mirror of the components; if a component restyles, update the veneer in the same PR.
  • Wireframes / loose concepts: when the UI does NOT exist yet or the idea is deliberately loose, compose from the .wire, .wire-bar, .wire-dot, .wire-fill primitives (hairline frames + muted skeleton shapes). Suggest the product, never counterfeit it: no real chrome colors, no fake screenshots.
  • Containers: invented panels sit on .surface (the house card).
  • Images: embed as data URIs (self-containment), radius 8px+, and the caption goes BELOW the frame, never padded inside it.
  • Quotes / testimonials: the .pullquote treatment, attribution in .mono-label beneath.

The reference for the spirit: /spec/demo.html sections 05 and 06 render an invented chart, a wireframe, a metrics row, and a conversation mockup with nothing but tokens + helpers. Scale complex specs the way that document does: group the TOC into labeled sections, mix kit sections with inventions, and let long documents grow more groups rather than flatter lists.

House rules (baked into the kit, obey in the copy)

The first three are founder rulings that got restated by hand run after run, so they are CATEGORIES with a test, not phrase lists to pattern-match. /spec Step 4.5 greps the finished file for them before it publishes; zero hits is the bar.

  • The TL;DR is never a giant paragraph (founder ruling 2026-07-25). The reader has ADHD; the card gets scanned, not read. Hard shape: ONE framing sentence of ~15 words, then 3 to 6 bullets, each ONE line with a <strong> lead. No nested lists, no second paragraph. Short sentences throughout the rest of the document too, and a body sentence past ~25 words gets split.
    • Test: delete everything after the <strong> leads. If the leads alone still say what is happening, the shape is right.
  • No meta discourse (founder ruling 2026-07-26, non-negotiable). The document never mentions the document. No sentence takes the spec, a section, a table, a list, or “we the authors” as its subject. Banned: “this document covers”, “this spec proposes”, “in this section”, “as mentioned above”, “the following table shows”, “we will describe” — and every variant, which is the point.
    • Test: the subject of every sentence is something in the PRODUCT (a user, a job, a route, a number), never the document or its authors. A sentence that stops being true when the doc is reorganized is meta discourse.
    • Before: “The following table shows the three dispatch modes we evaluated.”
    • After: “Three dispatch modes, one row each.” (then the table)
  • No tour-guide writing (founder ruling 2026-07-26, non-negotiable). Never narrate the reader’s path through the page, and never announce a fact before stating it. Banned: “let’s look at”, “now we turn to”, “as you can see”, “note that”, “first, we”, “keep in mind”, “here’s how it works”.
    • Test: every sentence survives being read alone, out of order, with no neighbours. A sentence whose only job is to introduce the next one gets deleted, and the next one stands.
    • Before: “Now let’s look at retries. As you can see, note that the backoff is capped.”
    • After: “Retries back off exponentially, capped at 60s.”
  • .deck and .lede are where those three break. Both slots ask for “framing”, and framing is what turns into meta discourse. A deck says what changes for the product and why now; a lede says what its section DECIDES. Neither describes the document.
  • No hedging. Banned: “likely”, “probably”, “seems”, “appears to”, “might be”, “could be”. A spec states decisions. Something unverified is said plainly to be unverified, or becomes a gate in a phase’s acceptance criteria.
  • No marketing register. No escalating-count tricolons (“seven entities, three tiers, one principle”), no taglines, no one-word-sentence titles, no dressed-up enumerations. Say what the thing IS.
  • No em dashes anywhere in the document copy; restructure the sentence. The two skeleton scripts carry em dashes in their comments and are still copied verbatim; never edit them to satisfy this rule.
  • Mono is only ever ALL-CAPS — the kit’s labels/eyebrows handle this; never put mono on mixed-case values, IDs, or URLs in body copy (.src-note and code are the exceptions, as code).
  • 8px spacing grid — the kit’s sections are on it; don’t add inline styles.
  • In PRODUCT copy the verb is “post”, never “publish” (a social platform gets posted to). Shipping the spec itself is still “publish”, per “Authoring with /spec” above; a spec is never “posted”. No industry jargon (“Comments”, not “Engagement”); no @ prefix on usernames.
  • Sections are numbered (01 · Name); every spec opens with the brand row, hero, and a TL;DR.
  • Specs do NOT live on the strata site (the site hosts the system). They live in docs/specs/<slug>.html, in the repo the work lives in.
  • Specs are published to a gist that is always secret, never public and never an Artifact. The gist id is the durable address: it is re-pushed in place forever, and each push is addressed by its own revision sha in ?v=. Secret means unlisted, not access-controlled; the link is the credential.

Raw markdown for agents: /docs/specs.md