OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      c87d79a27c7c
DATE      2026-07-17
SUBJECT   web: toc, accordion, timeline, toast, and diff fences
FILES     22 CHANGED
HASH      c87d79a27c7c
DATE      2026-07-17
SUBJECT   web: toc, accordion, timeline, toast,
          and diff fences
FILES     22 CHANGED
 

diff --git a/ooknet-design/src/components/Accordion/Accordion.astro b/ooknet-desig
n/src/components/Accordion/Accordion.astro
new file mode 100644
index 0000000..79bf6b7
--- /dev/null
+++ b/ooknet-design/src/components/Accordion/Accordion.astro
@@ -0,0 +1,13 @@
+---
+// Native <details> with ASCII chrome. Renders once - the [+] marker
+// and left rail are width-independent, so no dual-render is needed and
+// open state survives viewport changes.
+import type { Props } from "./Accordion";
+import "./Accordion.scss";
+
+const { label, open = false } = Astro.props as Props;
+---
+<details class="accordion" open={open}>
+  <summary class="acc-summary">{label}</summary>
+  <div class="acc-body"><slot /></div>
+</details>

diff --git a/ooknet-design/src/components/Ac
cordion/Accordion.astro b/ooknet-design/src/
components/Accordion/Accordion.astro
new file mode 100644
index 0000000..79bf6b7
--- /dev/null
+++ b/ooknet-design/src/components/Accordion
/Accordion.astro
@@ -0,0 +1,13 @@
+---
+// Native <details> with ASCII chrome. Rend
ers once - the [+] marker
+// and left rail are width-independent, so 
no dual-render is needed and
+// open state survives viewport changes.
+import type { Props } from "./Accordion";
+import "./Accordion.scss";
+
+const { label, open = false } = Astro.props
 as Props;
+---
+<details class="accordion" open={open}>
+  <summary class="acc-summary">{label}</sum
mary>
+  <div class="acc-body"><slot /></div>
+</details>
 

diff --git a/ooknet-design/src/components/Accordion/Accordion.scss b/ooknet-design
/src/components/Accordion/Accordion.scss
new file mode 100644
index 0000000..04c1858
--- /dev/null
+++ b/ooknet-design/src/components/Accordion/Accordion.scss
@@ -0,0 +1,65 @@
+.accordion {
+  font-family: var(--mono);
+  font-size: var(--fs);
+  line-height: var(--lh);
+  max-width: var(--frame-w);
+  margin: 0.4em 0;
+
+  .acc-summary {
+    cursor: pointer;
+    list-style: none;
+    font-weight: 700;
+    text-transform: uppercase;
+    white-space: pre;
+
+    &::-webkit-details-marker {
+      display: none;
+    }
+
+    &::before {
+      content: "[+] ";
+      font-family: var(--mono-frame);
+      font-weight: 400;
+    }
+
+    &:hover {
+      text-decoration: underline;
+      text-underline-offset: 3px;
+    }
+
+    &:focus-visible {
+      outline: 1px dashed var(--ink);
+      outline-offset: 1px;
+    }
+  }
+
+  &[open] > .acc-summary::before {
+    content: "[-] ";
+  }
+
+  .acc-body {
+    position: relative;
+    margin: 0.4em 0 0.8em;
+    padding-left: 4ch;
+
+    // Same glyph rail as blockquotes, seated under the marker column.
+    &::before {
+      content: "│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\
A│\A│\A│\A│\A│\A│\A│\A│";
+      position: absolute;
+      left: 1ch;
+      top: 0;
+      bottom: 0;
+      font-family: var(--mono-frame);
+      color: var(--ink);
+      white-space: pre;
+      overflow: hidden;
+    }
+
+    p {
+      margin: 0 0 0.6em;
+    }
+    p:last-child {
+      margin-bottom: 0;
+    }
+  }
+}

diff --git a/ooknet-design/src/components/Ac
cordion/Accordion.scss b/ooknet-design/src/c
omponents/Accordion/Accordion.scss
new file mode 100644
index 0000000..04c1858
--- /dev/null
+++ b/ooknet-design/src/components/Accordion
/Accordion.scss
@@ -0,0 +1,65 @@
+.accordion {
+  font-family: var(--mono);
+  font-size: var(--fs);
+  line-height: var(--lh);
+  max-width: var(--frame-w);
+  margin: 0.4em 0;
+
+  .acc-summary {
+    cursor: pointer;
+    list-style: none;
+    font-weight: 700;
+    text-transform: uppercase;
+    white-space: pre;
+
+    &::-webkit-details-marker {
+      display: none;
+    }
+
+    &::before {
+      content: "[+] ";
+      font-family: var(--mono-frame);
+      font-weight: 400;
+    }
+
+    &:hover {
+      text-decoration: underline;
+      text-underline-offset: 3px;
+    }
+
+    &:focus-visible {
+      outline: 1px dashed var(--ink);
+      outline-offset: 1px;
+    }
+  }
+
+  &[open] > .acc-summary::before {
+    content: "[-] ";
+  }
+
+  .acc-body {
+    position: relative;
+    margin: 0.4em 0 0.8em;
+    padding-left: 4ch;
+
+    // Same glyph rail as blockquotes, seat
ed under the marker column.
+    &::before {
+      content: "│\A│\A│\A│\A│\A│\A│\A│\A│\A
│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\A│\
A│\A│\A│\A│\A│\A│";
+      position: absolute;
+      left: 1ch;
+      top: 0;
+      bottom: 0;
+      font-family: var(--mono-frame);
+      color: var(--ink);
+      white-space: pre;
+      overflow: hidden;
+    }
+
+    p {
+      margin: 0 0 0.6em;
+    }
+    p:last-child {
+      margin-bottom: 0;
+    }
+  }
+}
 

diff --git a/ooknet-design/src/components/Accordion/Accordion.ts b/ooknet-design/s
rc/components/Accordion/Accordion.ts
new file mode 100644
index 0000000..bfc62b2
--- /dev/null
+++ b/ooknet-design/src/components/Accordion/Accordion.ts
@@ -0,0 +1,4 @@
+export interface Props {
+  label: string;
+  open?: boolean;
+}

diff --git a/ooknet-design/src/components/Ac
cordion/Accordion.ts b/ooknet-design/src/com
ponents/Accordion/Accordion.ts
new file mode 100644
index 0000000..bfc62b2
--- /dev/null
+++ b/ooknet-design/src/components/Accordion
/Accordion.ts
@@ -0,0 +1,4 @@
+export interface Props {
+  label: string;
+  open?: boolean;
+}
 

diff --git a/ooknet-design/src/components/Prose/Prose.scss b/ooknet-design/src/com
ponents/Prose/Prose.scss
index f2efa34..7319592 100644
--- a/ooknet-design/src/components/Prose/Prose.scss
+++ b/ooknet-design/src/components/Prose/Prose.scss
@@ -161,6 +161,11 @@
       pointer-events: none;
     }
 
+    // ```diff fences - monochrome: adds bold, everything else soft.
+    .diff-add { font-weight: 700; }
+    .diff-del { color: var(--ink-soft); }
+    .diff-hunk, .diff-meta { color: var(--ink-soft); font-style: italic; }
+
     .code-copy {
       font: inherit;
       color: var(--ink-soft);

diff --git a/ooknet-design/src/components/Pr
ose/Prose.scss b/ooknet-design/src/component
s/Prose/Prose.scss
index f2efa34..7319592 100644
--- a/ooknet-design/src/components/Prose/Pro
se.scss
+++ b/ooknet-design/src/components/Prose/Pro
se.scss
@@ -161,6 +161,11 @@
       pointer-events: none;
     }
 
+    // ```diff fences - monochrome: adds bo
ld, everything else soft.
+    .diff-add { font-weight: 700; }
+    .diff-del { color: var(--ink-soft); }
+    .diff-hunk, .diff-meta { color: var(--i
nk-soft); font-style: italic; }
+
     .code-copy {
       font: inherit;
       color: var(--ink-soft);
 

diff --git a/ooknet-design/src/components/TableOfContents/TableOfContents.astro b/
ooknet-design/src/components/TableOfContents/TableOfContents.astro
new file mode 100644
index 0000000..3df71c2
--- /dev/null
+++ b/ooknet-design/src/components/TableOfContents/TableOfContents.astro
@@ -0,0 +1,25 @@
+---
+// Dual-rendered contents block. Rows carry links, so they render as
+// per-row Pres (like Tree) rather than through Frame.
+import Pre from "../Pre/Pre.astro";
+import Frame from "../Frame/Frame.astro";
+import { rule, row2 } from "../../lib/ascii";
+import { FRAME_W, MOBILE_FRAME_W } from "../../lib/config";
+import { layoutToc, type Props, type TocRow } from "./TableOfContents";
+import "./TableOfContents.scss";
+
+const { headings, maxDepth = 3 } = Astro.props as Props;
+const wide = layoutToc(headings, FRAME_W, maxDepth);
+const narrow = layoutToc(headings, MOBILE_FRAME_W, maxDepth);
+
+const row = (r: TocRow, variant: string) => ({ r, variant });
+const rows = [...wide.map((r) => row(r, "frame-wide")), ...narrow.map((r) => row(
r, "frame-narrow"))];
+---
+{wide.length > 0 && (
+  <nav class="toc" aria-label="Contents">
+    <Frame build={(w) => [row2("CONTENTS", `${wide.length} SECTIONS`, w), rule(w)
].join("\n")} />
+    {rows.map(({ r, variant }) => (
+      <Pre class={`${variant} toc-row`}>{r.indent}<a class="toc-link" href={`#${r
.slug}`}>{r.title}</a><span class="toc-tail" aria-hidden="true">{` ${r.leader} ${r
.marker}`}</span></Pre>
+    ))}
+  </nav>
+)}

diff --git a/ooknet-design/src/components/Ta
bleOfContents/TableOfContents.astro b/ooknet
-design/src/components/TableOfContents/Table
OfContents.astro
new file mode 100644
index 0000000..3df71c2
--- /dev/null
+++ b/ooknet-design/src/components/TableOfCo
ntents/TableOfContents.astro
@@ -0,0 +1,25 @@
+---
+// Dual-rendered contents block. Rows carry
 links, so they render as
+// per-row Pres (like Tree) rather than thr
ough Frame.
+import Pre from "../Pre/Pre.astro";
+import Frame from "../Frame/Frame.astro";
+import { rule, row2 } from "../../lib/ascii
";
+import { FRAME_W, MOBILE_FRAME_W } from "..
/../lib/config";
+import { layoutToc, type Props, type TocRow
 } from "./TableOfContents";
+import "./TableOfContents.scss";
+
+const { headings, maxDepth = 3 } = Astro.pr
ops as Props;
+const wide = layoutToc(headings, FRAME_W, m
axDepth);
+const narrow = layoutToc(headings, MOBILE_F
RAME_W, maxDepth);
+
+const row = (r: TocRow, variant: string) =>
 ({ r, variant });
+const rows = [...wide.map((r) => row(r, "fr
ame-wide")), ...narrow.map((r) => row(r, "fr
ame-narrow"))];
+---
+{wide.length > 0 && (
+  <nav class="toc" aria-label="Contents">
+    <Frame build={(w) => [row2("CONTENTS", 
`${wide.length} SECTIONS`, w), rule(w)].join
("\n")} />
+    {rows.map(({ r, variant }) => (
+      <Pre class={`${variant} toc-row`}>{r.
indent}<a class="toc-link" href={`#${r.slug}
`}>{r.title}</a><span class="toc-tail" aria-
hidden="true">{` ${r.leader} ${r.marker}`}</
span></Pre>
+    ))}
+  </nav>
+)}
 

diff --git a/ooknet-design/src/components/TableOfContents/TableOfContents.scss b/o
oknet-design/src/components/TableOfContents/TableOfContents.scss
new file mode 100644
index 0000000..d72e77d
--- /dev/null
+++ b/ooknet-design/src/components/TableOfContents/TableOfContents.scss
@@ -0,0 +1,23 @@
+.toc {
+  display: block;
+  margin: 1.2em 0;
+}
+
+.toc-link {
+  color: var(--ink);
+  text-decoration: none;
+
+  &:hover {
+    text-decoration: underline;
+    text-underline-offset: 3px;
+  }
+
+  &:focus-visible {
+    outline: 1px dashed var(--ink);
+    outline-offset: 1px;
+  }
+}
+
+.toc-tail {
+  color: var(--ink-soft);
+}

diff --git a/ooknet-design/src/components/Ta
bleOfContents/TableOfContents.scss b/ooknet-
design/src/components/TableOfContents/TableO
fContents.scss
new file mode 100644
index 0000000..d72e77d
--- /dev/null
+++ b/ooknet-design/src/components/TableOfCo
ntents/TableOfContents.scss
@@ -0,0 +1,23 @@
+.toc {
+  display: block;
+  margin: 1.2em 0;
+}
+
+.toc-link {
+  color: var(--ink);
+  text-decoration: none;
+
+  &:hover {
+    text-decoration: underline;
+    text-underline-offset: 3px;
+  }
+
+  &:focus-visible {
+    outline: 1px dashed var(--ink);
+    outline-offset: 1px;
+  }
+}
+
+.toc-tail {
+  color: var(--ink-soft);
+}
 

diff --git a/ooknet-design/src/components/TableOfContents/TableOfContents.test.ts 
b/ooknet-design/src/components/TableOfContents/TableOfContents.test.ts
new file mode 100644
index 0000000..5a71fc7
--- /dev/null
+++ b/ooknet-design/src/components/TableOfContents/TableOfContents.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it } from "vitest";
+import { layoutToc } from "./TableOfContents";
+import { len } from "../../lib/ascii";
+
+const H = (depth: number, slug: string, text: string) => ({ depth, slug, text });
+const line = (r: ReturnType<typeof layoutToc>[number]) =>
+  `${r.indent}${r.title} ${r.leader} ${r.marker}`;
+
+describe("layoutToc", () => {
+  it("numbers sections hierarchically", () => {
+    const rows = layoutToc([H(1, "a", "A"), H(2, "b", "B"), H(2, "c", "C"), H(1, 
"d", "D")], 48);
+    expect(rows.map((r) => r.marker)).toEqual(["?1", "?1.1", "?1.2", "?2"]);
+  });
+
+  it("fills the exact width at any frame size", () => {
+    for (const w of [48, 86]) {
+      const rows = layoutToc([H(1, "a", "Alpha"), H(2, "b", "Beta section")], w);
+      for (const r of rows) expect(len(line(r))).toBe(w);
+    }
+  });
+
+  it("normalizes indentation to the shallowest heading", () => {
+    const rows = layoutToc([H(2, "a", "A"), H(3, "b", "B")], 48);
+    expect(rows[0].indent).toBe("");
+    expect(rows[1].indent).toBe("  ");
+  });
+
+  it("uppercases and collapses whitespace in titles", () => {
+    const rows = layoutToc([H(1, "a", "wide  tables")], 48);
+    expect(rows[0].title).toBe("WIDE TABLES");
+  });
+
+  it("strips head-rule glyphs leaked into extracted heading text", () => {
+    const rows = layoutToc([H(1, "a", `Purpose${"─".repeat(86)}`)], 48);
+    expect(rows[0].title).toBe("PURPOSE");
+  });
+
+  it("truncates long titles with an ellipsis and still fits", () => {
+    const rows = layoutToc([H(1, "a", "x".repeat(60))], 48);
+    expect(rows[0].title.endsWith("...")).toBe(true);
+    expect(len(line(rows[0]))).toBe(48);
+  });
+
+  it("drops headings deeper than maxDepth", () => {
+    expect(layoutToc([H(1, "a", "A"), H(4, "b", "B")], 48)).toHaveLength(1);
+  });
+});

diff --git a/ooknet-design/src/components/Ta
bleOfContents/TableOfContents.test.ts b/ookn
et-design/src/components/TableOfContents/Tab
leOfContents.test.ts
new file mode 100644
index 0000000..5a71fc7
--- /dev/null
+++ b/ooknet-design/src/components/TableOfCo
ntents/TableOfContents.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it } from "vites
t";
+import { layoutToc } from "./TableOfContent
s";
+import { len } from "../../lib/ascii";
+
+const H = (depth: number, slug: string, tex
t: string) => ({ depth, slug, text });
+const line = (r: ReturnType<typeof layoutTo
c>[number]) =>
+  `${r.indent}${r.title} ${r.leader} ${r.ma
rker}`;
+
+describe("layoutToc", () => {
+  it("numbers sections hierarchically", () 
=> {
+    const rows = layoutToc([H(1, "a", "A"),
 H(2, "b", "B"), H(2, "c", "C"), H(1, "d", "
D")], 48);
+    expect(rows.map((r) => r.marker)).toEqu
al(["?1", "?1.1", "?1.2", "?2"]);
+  });
+
+  it("fills the exact width at any frame si
ze", () => {
+    for (const w of [48, 86]) {
+      const rows = layoutToc([H(1, "a", "Al
pha"), H(2, "b", "Beta section")], w);
+      for (const r of rows) expect(len(line
(r))).toBe(w);
+    }
+  });
+
+  it("normalizes indentation to the shallow
est heading", () => {
+    const rows = layoutToc([H(2, "a", "A"),
 H(3, "b", "B")], 48);
+    expect(rows[0].indent).toBe("");
+    expect(rows[1].indent).toBe("  ");
+  });
+
+  it("uppercases and collapses whitespace i
n titles", () => {
+    const rows = layoutToc([H(1, "a", "wide
  tables")], 48);
+    expect(rows[0].title).toBe("WIDE TABLES
");
+  });
+
+  it("strips head-rule glyphs leaked into e
xtracted heading text", () => {
+    const rows = layoutToc([H(1, "a", `Purp
ose${"─".repeat(86)}`)], 48);
+    expect(rows[0].title).toBe("PURPOSE");
+  });
+
+  it("truncates long titles with an ellipsi
s and still fits", () => {
+    const rows = layoutToc([H(1, "a", "x".r
epeat(60))], 48);
+    expect(rows[0].title.endsWith("...")).t
oBe(true);
+    expect(len(line(rows[0]))).toBe(48);
+  });
+
+  it("drops headings deeper than maxDepth",
 () => {
+    expect(layoutToc([H(1, "a", "A"), H(4, 
"b", "B")], 48)).toHaveLength(1);
+  });
+});
 

diff --git a/ooknet-design/src/components/TableOfContents/TableOfContents.ts b/ook
net-design/src/components/TableOfContents/TableOfContents.ts
new file mode 100644
index 0000000..d279f1e
--- /dev/null
+++ b/ooknet-design/src/components/TableOfContents/TableOfContents.ts
@@ -0,0 +1,53 @@
+// Pure TOC layout: markdown headings ? numbered rows with dot leaders.
+// Returns parts (indent / title / leader / marker) so the component can
+// wrap the title in a link.
+import { checkGrid, len } from "../../lib/ascii";
+
+export interface TocHeading {
+  depth: number;
+  slug: string;
+  text: string;
+}
+
+export interface TocRow {
+  indent: string;
+  title: string;
+  /** Dot leader between title and marker. */
+  leader: string;
+  /** ?-style section number. */
+  marker: string;
+  slug: string;
+}
+
+export interface Props {
+  headings: TocHeading[];
+  maxDepth?: number;
+}
+
+const MIN_DOTS = 3;
+
+export function layoutToc(headings: TocHeading[], width: number, maxDepth = 3): T
ocRow[] {
+  const hs = headings.filter((h) => h.depth <= maxDepth);
+  if (!hs.length) return [];
+
+  const min = Math.min(...hs.map((h) => h.depth));
+  const counters: number[] = [];
+
+  return hs.map((h) => {
+    const level = h.depth - min;
+    counters.splice(level + 1);
+    counters[level] = (counters[level] ?? 0) + 1;
+    const marker = `?${counters.slice(0, level + 1).map((c) => c ?? 1).join(".")}
`;
+
+    const indent = "  ".repeat(level);
+    // Astro extracts heading text after rehype-ascii has appended the
+    // h1 head-rule, so box-drawing runs must be stripped back out.
+    let title = h.text.replace(/[─-▟]+/gu, "").replace(/\s+/g, " ").trim().toUppe
rCase();
+    const maxTitle = width - len(indent) - len(marker) - 2 - MIN_DOTS;
+    if (len(title) > maxTitle) title = [...title].slice(0, maxTitle - 1).join("")
 + "...";
+
+    const leader = ".".repeat(width - len(indent) - len(title) - len(marker) - 2)
;
+    checkGrid(`${indent}${title} ${leader} ${marker}`, width, "toc row");
+    return { indent, title, leader, marker, slug: h.slug };
+  });
+}

diff --git a/ooknet-design/src/components/Ta
bleOfContents/TableOfContents.ts b/ooknet-de
sign/src/components/TableOfContents/TableOfC
ontents.ts
new file mode 100644
index 0000000..d279f1e
--- /dev/null
+++ b/ooknet-design/src/components/TableOfCo
ntents/TableOfContents.ts
@@ -0,0 +1,53 @@
+// Pure TOC layout: markdown headings ? num
bered rows with dot leaders.
+// Returns parts (indent / title / leader /
 marker) so the component can
+// wrap the title in a link.
+import { checkGrid, len } from "../../lib/a
scii";
+
+export interface TocHeading {
+  depth: number;
+  slug: string;
+  text: string;
+}
+
+export interface TocRow {
+  indent: string;
+  title: string;
+  /** Dot leader between title and marker. 
*/
+  leader: string;
+  /** ?-style section number. */
+  marker: string;
+  slug: string;
+}
+
+export interface Props {
+  headings: TocHeading[];
+  maxDepth?: number;
+}
+
+const MIN_DOTS = 3;
+
+export function layoutToc(headings: TocHead
ing[], width: number, maxDepth = 3): TocRow[
] {
+  const hs = headings.filter((h) => h.depth
 <= maxDepth);
+  if (!hs.length) return [];
+
+  const min = Math.min(...hs.map((h) => h.d
epth));
+  const counters: number[] = [];
+
+  return hs.map((h) => {
+    const level = h.depth - min;
+    counters.splice(level + 1);
+    counters[level] = (counters[level] ?? 0
) + 1;
+    const marker = `?${counters.slice(0, le
vel + 1).map((c) => c ?? 1).join(".")}`;
+
+    const indent = "  ".repeat(level);
+    // Astro extracts heading text after re
hype-ascii has appended the
+    // h1 head-rule, so box-drawing runs mu
st be stripped back out.
+    let title = h.text.replace(/[─-▟]+/gu, 
"").replace(/\s+/g, " ").trim().toUpperCase(
);
+    const maxTitle = width - len(indent) - 
len(marker) - 2 - MIN_DOTS;
+    if (len(title) > maxTitle) title = [...
title].slice(0, maxTitle - 1).join("") + "..
.";
+
+    const leader = ".".repeat(width - len(i
ndent) - len(title) - len(marker) - 2);
+    checkGrid(`${indent}${title} ${leader} 
${marker}`, width, "toc row");
+    return { indent, title, leader, marker,
 slug: h.slug };
+  });
+}
 

diff --git a/ooknet-design/src/components/Timeline/Timeline.astro b/ooknet-design/
src/components/Timeline/Timeline.astro
new file mode 100644
index 0000000..dfdc6aa
--- /dev/null
+++ b/ooknet-design/src/components/Timeline/Timeline.astro
@@ -0,0 +1,22 @@
+---
+import Pre from "../Pre/Pre.astro";
+import { FRAME_W, MOBILE_FRAME_W } from "../../lib/config";
+import { layoutTimeline, type Props, type TimelineRow } from "./Timeline";
+import "./Timeline.scss";
+
+const { entries } = Astro.props as Props;
+
+const variants: [string, TimelineRow[]][] = [
+  ["frame-wide", layoutTimeline(entries, FRAME_W)],
+  ["frame-narrow", layoutTimeline(entries, MOBILE_FRAME_W)],
+];
+---
+{variants.map(([variant, rows]) =>
+  rows.map((r) => (
+    <Pre class={`${variant} timeline-row`}>{r.prefix}{
+      r.href ? <a class="timeline-link" href={r.href}>{r.text}</a>
+      : r.soft ? <span class="timeline-soft">{r.text}</span>
+      : r.text
+    }</Pre>
+  ))
+)}

diff --git a/ooknet-design/src/components/Ti
meline/Timeline.astro b/ooknet-design/src/co
mponents/Timeline/Timeline.astro
new file mode 100644
index 0000000..dfdc6aa
--- /dev/null
+++ b/ooknet-design/src/components/Timeline/
Timeline.astro
@@ -0,0 +1,22 @@
+---
+import Pre from "../Pre/Pre.astro";
+import { FRAME_W, MOBILE_FRAME_W } from "..
/../lib/config";
+import { layoutTimeline, type Props, type T
imelineRow } from "./Timeline";
+import "./Timeline.scss";
+
+const { entries } = Astro.props as Props;
+
+const variants: [string, TimelineRow[]][] =
 [
+  ["frame-wide", layoutTimeline(entries, FR
AME_W)],
+  ["frame-narrow", layoutTimeline(entries, 
MOBILE_FRAME_W)],
+];
+---
+{variants.map(([variant, rows]) =>
+  rows.map((r) => (
+    <Pre class={`${variant} timeline-row`}>
{r.prefix}{
+      r.href ? <a class="timeline-link" hre
f={r.href}>{r.text}</a>
+      : r.soft ? <span class="timeline-soft
">{r.text}</span>
+      : r.text
+    }</Pre>
+  ))
+)}
 

diff --git a/ooknet-design/src/components/Timeline/Timeline.scss b/ooknet-design/s
rc/components/Timeline/Timeline.scss
new file mode 100644
index 0000000..9b50ff8
--- /dev/null
+++ b/ooknet-design/src/components/Timeline/Timeline.scss
@@ -0,0 +1,18 @@
+.timeline-link {
+  color: var(--ink);
+  text-decoration: none;
+
+  &:hover {
+    text-decoration: underline;
+    text-underline-offset: 3px;
+  }
+
+  &:focus-visible {
+    outline: 1px dashed var(--ink);
+    outline-offset: 1px;
+  }
+}
+
+.timeline-soft {
+  color: var(--ink-soft);
+}

diff --git a/ooknet-design/src/components/Ti
meline/Timeline.scss b/ooknet-design/src/com
ponents/Timeline/Timeline.scss
new file mode 100644
index 0000000..9b50ff8
--- /dev/null
+++ b/ooknet-design/src/components/Timeline/
Timeline.scss
@@ -0,0 +1,18 @@
+.timeline-link {
+  color: var(--ink);
+  text-decoration: none;
+
+  &:hover {
+    text-decoration: underline;
+    text-underline-offset: 3px;
+  }
+
+  &:focus-visible {
+    outline: 1px dashed var(--ink);
+    outline-offset: 1px;
+  }
+}
+
+.timeline-soft {
+  color: var(--ink-soft);
+}
 

diff --git a/ooknet-design/src/components/Timeline/Timeline.test.ts b/ooknet-desig
n/src/components/Timeline/Timeline.test.ts
new file mode 100644
index 0000000..03f6c38
--- /dev/null
+++ b/ooknet-design/src/components/Timeline/Timeline.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, it } from "vitest";
+import { layoutTimeline } from "./Timeline";
+import { G, len } from "../../lib/ascii";
+
+const E = (date: string, title: string, desc?: string) => ({ date, title, desc })
;
+
+describe("layoutTimeline", () => {
+  it("opens the spine with ┬ and continues with ┼", () => {
+    const rows = layoutTimeline([E("2026-01-01", "A"), E("2026-01-02", "B"), E("2
026-01-03", "C")], 48);
+    const junctions = rows.filter((r) => !r.soft).map((r) => [...r.prefix][12]);
+    expect(junctions).toEqual([G.TJ, G.X, G.X]);
+  });
+
+  it("renders a lone entry without a spine", () => {
+    const rows = layoutTimeline([E("2026-01-01", "ONLY")], 48);
+    expect(rows).toHaveLength(1);
+    expect(rows[0].prefix).toContain(`${G.H}${G.H}${G.H}`);
+  });
+
+  it("aligns the rail under the junction", () => {
+    const rows = layoutTimeline([E("2026-01-01", "A", "some detail"), E("2026-01-
02", "B")], 48);
+    const jCol = [...rows[0].prefix].indexOf(G.TJ);
+    expect([...rows[1].prefix].indexOf(G.V)).toBe(jCol);
+  });
+
+  it("wraps descriptions inside the frame", () => {
+    const rows = layoutTimeline([E("2026-01-01", "A", "word ".repeat(30).trim()),
 E("2026-01-02", "B")], 48);
+    for (const r of rows) expect(len(r.prefix + r.text)).toBeLessThanOrEqual(48);
+    expect(rows.filter((r) => r.soft && r.text).length).toBeGreaterThan(1);
+  });
+
+  it("truncates overlong titles with an ellipsis", () => {
+    const rows = layoutTimeline([E("2026-01-01", "x".repeat(80)), E("2026-01-02",
 "B")], 48);
+    expect(rows[0].text.endsWith("...")).toBe(true);
+    expect(len(rows[0].prefix + rows[0].text)).toBeLessThanOrEqual(48);
+  });
+});

diff --git a/ooknet-design/src/components/Ti
meline/Timeline.test.ts b/ooknet-design/src/
components/Timeline/Timeline.test.ts
new file mode 100644
index 0000000..03f6c38
--- /dev/null
+++ b/ooknet-design/src/components/Timeline/
Timeline.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, it } from "vites
t";
+import { layoutTimeline } from "./Timeline"
;
+import { G, len } from "../../lib/ascii";
+
+const E = (date: string, title: string, des
c?: string) => ({ date, title, desc });
+
+describe("layoutTimeline", () => {
+  it("opens the spine with ┬ and continues 
with ┼", () => {
+    const rows = layoutTimeline([E("2026-01
-01", "A"), E("2026-01-02", "B"), E("2026-01
-03", "C")], 48);
+    const junctions = rows.filter((r) => !r
.soft).map((r) => [...r.prefix][12]);
+    expect(junctions).toEqual([G.TJ, G.X, G
.X]);
+  });
+
+  it("renders a lone entry without a spine"
, () => {
+    const rows = layoutTimeline([E("2026-01
-01", "ONLY")], 48);
+    expect(rows).toHaveLength(1);
+    expect(rows[0].prefix).toContain(`${G.H
}${G.H}${G.H}`);
+  });
+
+  it("aligns the rail under the junction", 
() => {
+    const rows = layoutTimeline([E("2026-01
-01", "A", "some detail"), E("2026-01-02", "
B")], 48);
+    const jCol = [...rows[0].prefix].indexO
f(G.TJ);
+    expect([...rows[1].prefix].indexOf(G.V)
).toBe(jCol);
+  });
+
+  it("wraps descriptions inside the frame",
 () => {
+    const rows = layoutTimeline([E("2026-01
-01", "A", "word ".repeat(30).trim()), E("20
26-01-02", "B")], 48);
+    for (const r of rows) expect(len(r.pref
ix + r.text)).toBeLessThanOrEqual(48);
+    expect(rows.filter((r) => r.soft && r.t
ext).length).toBeGreaterThan(1);
+  });
+
+  it("truncates overlong titles with an ell
ipsis", () => {
+    const rows = layoutTimeline([E("2026-01
-01", "x".repeat(80)), E("2026-01-02", "B")]
, 48);
+    expect(rows[0].text.endsWith("...")).to
Be(true);
+    expect(len(rows[0].prefix + rows[0].tex
t)).toBeLessThanOrEqual(48);
+  });
+});
 

diff --git a/ooknet-design/src/components/Timeline/Timeline.ts b/ooknet-design/src
/components/Timeline/Timeline.ts
new file mode 100644
index 0000000..e1ef506
--- /dev/null
+++ b/ooknet-design/src/components/Timeline/Timeline.ts
@@ -0,0 +1,53 @@
+// Vertical timeline: date column, spine junction, entry title, wrapped
+// description beside the rail. Returns rows so the component can link
+// titles.
+import { G, checkGrid, len, pad, wrap } from "../../lib/ascii";
+
+export interface TimelineEntry {
+  date: string;
+  title: string;
+  desc?: string;
+  href?: string;
+}
+
+export interface TimelineRow {
+  prefix: string;
+  text: string;
+  href?: string;
+  /** Description / spacer rows render in the soft ink. */
+  soft?: boolean;
+}
+
+export interface Props {
+  entries: TimelineEntry[];
+}
+
+export function layoutTimeline(entries: TimelineEntry[], width: number): Timeline
Row[] {
+  if (!entries.length) return [];
+
+  const dateW = Math.max(...entries.map((e) => len(e.date)));
+  const textW = width - dateW - 5;
+  const rail = `${" ".repeat(dateW + 2)}${G.V}  `;
+  const out: TimelineRow[] = [];
+
+  entries.forEach((e, i) => {
+    // ┬ opens the spine, ┼ continues it; a lone entry needs no spine.
+    const j = entries.length === 1 ? G.H : i === 0 ? G.TJ : G.X;
+    const title = len(e.title) > textW ? [...e.title].slice(0, textW - 1).join(""
) + "..." : e.title;
+    const prefix = `${pad(e.date, dateW)} ${G.H}${j}${G.H} `;
+    checkGrid(prefix + title, width, "timeline entry");
+    out.push({ prefix, text: title, href: e.href });
+
+    if (e.desc) {
+      for (const ln of wrap(e.desc, textW).split("\n")) {
+        checkGrid(rail + ln, width, "timeline desc");
+        out.push({ prefix: rail, text: ln, soft: true });
+      }
+    }
+    if (i < entries.length - 1) {
+      out.push({ prefix: `${" ".repeat(dateW + 2)}${G.V}`, text: "", soft: true }
);
+    }
+  });
+
+  return out;
+}

diff --git a/ooknet-design/src/components/Ti
meline/Timeline.ts b/ooknet-design/src/compo
nents/Timeline/Timeline.ts
new file mode 100644
index 0000000..e1ef506
--- /dev/null
+++ b/ooknet-design/src/components/Timeline/
Timeline.ts
@@ -0,0 +1,53 @@
+// Vertical timeline: date column, spine ju
nction, entry title, wrapped
+// description beside the rail. Returns row
s so the component can link
+// titles.
+import { G, checkGrid, len, pad, wrap } fro
m "../../lib/ascii";
+
+export interface TimelineEntry {
+  date: string;
+  title: string;
+  desc?: string;
+  href?: string;
+}
+
+export interface TimelineRow {
+  prefix: string;
+  text: string;
+  href?: string;
+  /** Description / spacer rows render in t
he soft ink. */
+  soft?: boolean;
+}
+
+export interface Props {
+  entries: TimelineEntry[];
+}
+
+export function layoutTimeline(entries: Tim
elineEntry[], width: number): TimelineRow[] 
{
+  if (!entries.length) return [];
+
+  const dateW = Math.max(...entries.map((e)
 => len(e.date)));
+  const textW = width - dateW - 5;
+  const rail = `${" ".repeat(dateW + 2)}${G
.V}  `;
+  const out: TimelineRow[] = [];
+
+  entries.forEach((e, i) => {
+    // ┬ opens the spine, ┼ continues it; a
 lone entry needs no spine.
+    const j = entries.length === 1 ? G.H : 
i === 0 ? G.TJ : G.X;
+    const title = len(e.title) > textW ? [.
..e.title].slice(0, textW - 1).join("") + ".
.." : e.title;
+    const prefix = `${pad(e.date, dateW)} $
{G.H}${j}${G.H} `;
+    checkGrid(prefix + title, width, "timel
ine entry");
+    out.push({ prefix, text: title, href: e
.href });
+
+    if (e.desc) {
+      for (const ln of wrap(e.desc, textW).
split("\n")) {
+        checkGrid(rail + ln, width, "timeli
ne desc");
+        out.push({ prefix: rail, text: ln, 
soft: true });
+      }
+    }
+    if (i < entries.length - 1) {
+      out.push({ prefix: `${" ".repeat(date
W + 2)}${G.V}`, text: "", soft: true });
+    }
+  });
+
+  return out;
+}
 

diff --git a/ooknet-design/src/components/Toast/Toast.astro b/ooknet-design/src/co
mponents/Toast/Toast.astro
new file mode 100644
index 0000000..09a32e1
--- /dev/null
+++ b/ooknet-design/src/components/Toast/Toast.astro
@@ -0,0 +1,41 @@
+---
+// Fixed-corner toast region. Fire with window.ooknetToast(message,
+// variant) or a "ooknet:toast" CustomEvent; frames are built
+// client-side by the same tested builder.
+import "./Toast.scss";
+---
+<div class="toast-region" aria-live="polite"></div>
+<script>
+  import { buildToast } from "./Toast";
+  import type { StatusVariant } from "../Status/Status";
+
+  const region = document.querySelector(".toast-region")!;
+
+  function toast(message: string, variant: StatusVariant = "ok") {
+    const el = document.createElement("div");
+    el.className = "toast";
+
+    const pre = document.createElement("pre");
+    pre.className = "ascii toast-frame";
+    pre.setAttribute("aria-hidden", "true");
+    pre.textContent = buildToast(message, variant);
+
+    const sr = document.createElement("span");
+    sr.className = "sr-only";
+    sr.textContent = message;
+
+    el.append(pre, sr);
+    region.append(el);
+    requestAnimationFrame(() => el.classList.add("toast-in"));
+    setTimeout(() => {
+      el.classList.remove("toast-in");
+      setTimeout(() => el.remove(), 250);
+    }, 2600);
+  }
+
+  window.ooknetToast = toast;
+  document.addEventListener("ooknet:toast", (e) => {
+    const d = (e as CustomEvent).detail ?? {};
+    if (d.message) toast(String(d.message), d.variant);
+  });
+</script>

diff --git a/ooknet-design/src/components/To
ast/Toast.astro b/ooknet-design/src/componen
ts/Toast/Toast.astro
new file mode 100644
index 0000000..09a32e1
--- /dev/null
+++ b/ooknet-design/src/components/Toast/Toa
st.astro
@@ -0,0 +1,41 @@
+---
+// Fixed-corner toast region. Fire with win
dow.ooknetToast(message,
+// variant) or a "ooknet:toast" CustomEvent
; frames are built
+// client-side by the same tested builder.
+import "./Toast.scss";
+---
+<div class="toast-region" aria-live="polite
"></div>
+<script>
+  import { buildToast } from "./Toast";
+  import type { StatusVariant } from "../St
atus/Status";
+
+  const region = document.querySelector(".t
oast-region")!;
+
+  function toast(message: string, variant: 
StatusVariant = "ok") {
+    const el = document.createElement("div"
);
+    el.className = "toast";
+
+    const pre = document.createElement("pre
");
+    pre.className = "ascii toast-frame";
+    pre.setAttribute("aria-hidden", "true")
;
+    pre.textContent = buildToast(message, v
ariant);
+
+    const sr = document.createElement("span
");
+    sr.className = "sr-only";
+    sr.textContent = message;
+
+    el.append(pre, sr);
+    region.append(el);
+    requestAnimationFrame(() => el.classLis
t.add("toast-in"));
+    setTimeout(() => {
+      el.classList.remove("toast-in");
+      setTimeout(() => el.remove(), 250);
+    }, 2600);
+  }
+
+  window.ooknetToast = toast;
+  document.addEventListener("ooknet:toast",
 (e) => {
+    const d = (e as CustomEvent).detail ?? 
{};
+    if (d.message) toast(String(d.message),
 d.variant);
+  });
+</script>
 

diff --git a/ooknet-design/src/components/Toast/Toast.scss b/ooknet-design/src/com
ponents/Toast/Toast.scss
new file mode 100644
index 0000000..3a203ae
--- /dev/null
+++ b/ooknet-design/src/components/Toast/Toast.scss
@@ -0,0 +1,28 @@
+.toast-region {
+  position: fixed;
+  right: 1.25em;
+  bottom: 1.25em;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  gap: 0.5em;
+  z-index: 10;
+  pointer-events: none;
+}
+
+.toast {
+  opacity: 0;
+  transform: translateY(4px);
+  transition: opacity 0.2s ease, transform 0.2s ease;
+
+  &.toast-in {
+    opacity: 1;
+    transform: none;
+  }
+
+  pre.ascii.toast-frame {
+    width: auto;
+    background: var(--paper);
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08), 0 8px 24px -12px rgba(0, 0, 0, 0.3
);
+  }
+}

diff --git a/ooknet-design/src/components/To
ast/Toast.scss b/ooknet-design/src/component
s/Toast/Toast.scss
new file mode 100644
index 0000000..3a203ae
--- /dev/null
+++ b/ooknet-design/src/components/Toast/Toa
st.scss
@@ -0,0 +1,28 @@
+.toast-region {
+  position: fixed;
+  right: 1.25em;
+  bottom: 1.25em;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  gap: 0.5em;
+  z-index: 10;
+  pointer-events: none;
+}
+
+.toast {
+  opacity: 0;
+  transform: translateY(4px);
+  transition: opacity 0.2s ease, transform 
0.2s ease;
+
+  &.toast-in {
+    opacity: 1;
+    transform: none;
+  }
+
+  pre.ascii.toast-frame {
+    width: auto;
+    background: var(--paper);
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.0
8), 0 8px 24px -12px rgba(0, 0, 0, 0.3);
+  }
+}
 

diff --git a/ooknet-design/src/components/Toast/Toast.test.ts b/ooknet-design/src/
components/Toast/Toast.test.ts
new file mode 100644
index 0000000..7492d9d
--- /dev/null
+++ b/ooknet-design/src/components/Toast/Toast.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it } from "vitest";
+import { buildToast, TOAST_W } from "./Toast";
+import { len } from "../../lib/ascii";
+
+describe("buildToast", () => {
+  it("renders every line at the fixed toast width", () => {
+    for (const line of buildToast("transmission queued").split("\n")) {
+      expect(len(line)).toBe(TOAST_W);
+    }
+  });
+
+  it("prefixes the status tag and uppercases the message", () => {
+    expect(buildToast("saved", "ok")).toContain("[ OK ] SAVED");
+    expect(buildToast("lost", "error")).toContain("[ !! ] LOST");
+  });
+
+  it("aligns wrapped continuation lines under the message", () => {
+    const lines = buildToast("a message long enough to wrap onto more lines").spl
it("\n");
+    expect(lines.length).toBeGreaterThan(3);
+    expect(lines[2]).toMatch(/^│        \S/);
+  });
+});

diff --git a/ooknet-design/src/components/To
ast/Toast.test.ts b/ooknet-design/src/compon
ents/Toast/Toast.test.ts
new file mode 100644
index 0000000..7492d9d
--- /dev/null
+++ b/ooknet-design/src/components/Toast/Toa
st.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it } from "vites
t";
+import { buildToast, TOAST_W } from "./Toas
t";
+import { len } from "../../lib/ascii";
+
+describe("buildToast", () => {
+  it("renders every line at the fixed toast
 width", () => {
+    for (const line of buildToast("transmis
sion queued").split("\n")) {
+      expect(len(line)).toBe(TOAST_W);
+    }
+  });
+
+  it("prefixes the status tag and uppercase
s the message", () => {
+    expect(buildToast("saved", "ok")).toCon
tain("[ OK ] SAVED");
+    expect(buildToast("lost", "error")).toC
ontain("[ !! ] LOST");
+  });
+
+  it("aligns wrapped continuation lines und
er the message", () => {
+    const lines = buildToast("a message lon
g enough to wrap onto more lines").split("\n
");
+    expect(lines.length).toBeGreaterThan(3)
;
+    expect(lines[2]).toMatch(/^│        \S/
);
+  });
+});
 

diff --git a/ooknet-design/src/components/Toast/Toast.ts b/ooknet-design/src/compo
nents/Toast/Toast.ts
new file mode 100644
index 0000000..5f5f9d7
--- /dev/null
+++ b/ooknet-design/src/components/Toast/Toast.ts
@@ -0,0 +1,22 @@
+// Toast frame builder. Runs in the browser (bundled into the Toast
+// component script), so it stays pure and small.
+import { box, len, wrap } from "../../lib/ascii";
+import { STATUS_TAG, type StatusVariant } from "../Status/Status";
+
+export const TOAST_W = 36;
+
+declare global {
+  interface Window {
+    ooknetToast?: (message: string, variant?: StatusVariant) => void;
+  }
+}
+
+export function buildToast(message: string, variant: StatusVariant = "ok"): strin
g {
+  const tag = STATUS_TAG[variant];
+  const msg = message.replace(/\s+/g, " ").trim().toUpperCase();
+  const bodyW = TOAST_W - 4 - len(tag) - 1;
+  const lines = wrap(msg, bodyW)
+    .split("\n")
+    .map((l, i) => (i === 0 ? ` ${tag} ${l}` : ` ${" ".repeat(len(tag))} ${l}`));
+  return box({ width: TOAST_W, lines, align: "end" });
+}

diff --git a/ooknet-design/src/components/To
ast/Toast.ts b/ooknet-design/src/components/
Toast/Toast.ts
new file mode 100644
index 0000000..5f5f9d7
--- /dev/null
+++ b/ooknet-design/src/components/Toast/Toa
st.ts
@@ -0,0 +1,22 @@
+// Toast frame builder. Runs in the browser
 (bundled into the Toast
+// component script), so it stays pure and 
small.
+import { box, len, wrap } from "../../lib/a
scii";
+import { STATUS_TAG, type StatusVariant } f
rom "../Status/Status";
+
+export const TOAST_W = 36;
+
+declare global {
+  interface Window {
+    ooknetToast?: (message: string, variant
?: StatusVariant) => void;
+  }
+}
+
+export function buildToast(message: string,
 variant: StatusVariant = "ok"): string {
+  const tag = STATUS_TAG[variant];
+  const msg = message.replace(/\s+/g, " ").
trim().toUpperCase();
+  const bodyW = TOAST_W - 4 - len(tag) - 1;
+  const lines = wrap(msg, bodyW)
+    .split("\n")
+    .map((l, i) => (i === 0 ? ` ${tag} ${l}
` : ` ${" ".repeat(len(tag))} ${l}`));
+  return box({ width: TOAST_W, lines, align
: "end" });
+}
 

diff --git a/ooknet-design/src/content/notes/n0-style-sheet.md b/ooknet-design/src
/content/notes/n0-style-sheet.md
index 1264e20..f2064fb 100644
--- a/ooknet-design/src/content/notes/n0-style-sheet.md
+++ b/ooknet-design/src/content/notes/n0-style-sheet.md
@@ -106,6 +106,18 @@ export function pad(s: unknown, n: number): string {
 }
 ```
 
+A `diff` fence renders additions bold and deletions soft - the
+palette stays monochrome:
+
+```diff
+@@ -1,3 +1,4 @@
+ export function pad(s: unknown, n: number): string {
+-  return String(s).padEnd(n);
++  const str = String(s ?? "");
++  return str + " ".repeat(Math.max(0, n - len(str)));
+ }
+```
+
 # Lists
 
 - frames are strings, components are chrome

diff --git a/ooknet-design/src/content/notes
/n0-style-sheet.md b/ooknet-design/src/conte
nt/notes/n0-style-sheet.md
index 1264e20..f2064fb 100644
--- a/ooknet-design/src/content/notes/n0-sty
le-sheet.md
+++ b/ooknet-design/src/content/notes/n0-sty
le-sheet.md
@@ -106,6 +106,18 @@ export function pad(s: 
unknown, n: number): string {
 }
 ```
 
+A `diff` fence renders additions bold and d
eletions soft - the
+palette stays monochrome:
+
+```diff
+@@ -1,3 +1,4 @@
+ export function pad(s: unknown, n: number)
: string {
+-  return String(s).padEnd(n);
++  const str = String(s ?? "");
++  return str + " ".repeat(Math.max(0, n - 
len(str)));
+ }
+```
+
 # Lists
 
 - frames are strings, components are chrome
 

diff --git a/ooknet-design/src/layouts/ArticleLayout/ArticleLayout.astro b/ooknet-
design/src/layouts/ArticleLayout/ArticleLayout.astro
index a363a6e..786ce49 100644
--- a/ooknet-design/src/layouts/ArticleLayout/ArticleLayout.astro
+++ b/ooknet-design/src/layouts/ArticleLayout/ArticleLayout.astro
@@ -8,6 +8,7 @@ import "@fontsource/cascadia-mono/400.css";
 import "@fontsource/cascadia-mono/700.css";
 import "../../styles/global.scss";
 import "./ArticleLayout.scss";
+import Toast from "../../components/Toast/Toast.astro";
 const { title = "OOKNET KB" } = Astro.props;
 ---
 <!doctype html>
@@ -31,6 +32,7 @@ const { title = "OOKNET KB" } = Astro.props;
     <div class="page">
       <slot />
     </div>
+    <Toast />
     <script is:inline>
       document.addEventListener("click", async (e) => {
         const copyBtn = e.target.closest(".code-copy");

diff --git a/ooknet-design/src/layouts/Artic
leLayout/ArticleLayout.astro b/ooknet-design
/src/layouts/ArticleLayout/ArticleLayout.ast
ro
index a363a6e..786ce49 100644
--- a/ooknet-design/src/layouts/ArticleLayou
t/ArticleLayout.astro
+++ b/ooknet-design/src/layouts/ArticleLayou
t/ArticleLayout.astro
@@ -8,6 +8,7 @@ import "@fontsource/cascadia
-mono/400.css";
 import "@fontsource/cascadia-mono/700.css";
 import "../../styles/global.scss";
 import "./ArticleLayout.scss";
+import Toast from "../../components/Toast/T
oast.astro";
 const { title = "OOKNET KB" } = Astro.props
;
 ---
 <!doctype html>
@@ -31,6 +32,7 @@ const { title = "OOKNET KB
" } = Astro.props;
     <div class="page">
       <slot />
     </div>
+    <Toast />
     <script is:inline>
       document.addEventListener("click", as
ync (e) => {
         const copyBtn = e.target.closest(".
code-copy");
 

diff --git a/ooknet-design/src/lib/rehype-ascii.test.ts b/ooknet-design/src/lib/re
hype-ascii.test.ts
new file mode 100644
index 0000000..7179e33
--- /dev/null
+++ b/ooknet-design/src/lib/rehype-ascii.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it } from "vitest";
+import { diffLineClass } from "./rehype-ascii";
+
+describe("diffLineClass", () => {
+  it("classifies additions and deletions", () => {
+    expect(diffLineClass("+  added line")).toBe("diff-add");
+    expect(diffLineClass("-  removed line")).toBe("diff-del");
+  });
+
+  it("classifies hunk headers and file metadata", () => {
+    expect(diffLineClass("@@ -1,4 +1,4 @@")).toBe("diff-hunk");
+    expect(diffLineClass("--- a/src/lib/ascii.ts")).toBe("diff-meta");
+    expect(diffLineClass("+++ b/src/lib/ascii.ts")).toBe("diff-meta");
+    expect(diffLineClass("diff --git a/x b/x")).toBe("diff-meta");
+    expect(diffLineClass("index 3f2d1a..9c8b7e 100644")).toBe("diff-meta");
+  });
+
+  it("leaves context lines unclassified", () => {
+    expect(diffLineClass("  const str = String(s);")).toBeNull();
+    expect(diffLineClass("")).toBeNull();
+  });
+});

diff --git a/ooknet-design/src/lib/rehype-as
cii.test.ts b/ooknet-design/src/lib/rehype-a
scii.test.ts
new file mode 100644
index 0000000..7179e33
--- /dev/null
+++ b/ooknet-design/src/lib/rehype-ascii.tes
t.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it } from "vites
t";
+import { diffLineClass } from "./rehype-asc
ii";
+
+describe("diffLineClass", () => {
+  it("classifies additions and deletions", 
() => {
+    expect(diffLineClass("+  added line")).
toBe("diff-add");
+    expect(diffLineClass("-  removed line")
).toBe("diff-del");
+  });
+
+  it("classifies hunk headers and file meta
data", () => {
+    expect(diffLineClass("@@ -1,4 +1,4 @@")
).toBe("diff-hunk");
+    expect(diffLineClass("--- a/src/lib/asc
ii.ts")).toBe("diff-meta");
+    expect(diffLineClass("+++ b/src/lib/asc
ii.ts")).toBe("diff-meta");
+    expect(diffLineClass("diff --git a/x b/
x")).toBe("diff-meta");
+    expect(diffLineClass("index 3f2d1a..9c8
b7e 100644")).toBe("diff-meta");
+  });
+
+  it("leaves context lines unclassified", (
) => {
+    expect(diffLineClass("  const str = Str
ing(s);")).toBeNull();
+    expect(diffLineClass("")).toBeNull();
+  });
+});
 

diff --git a/ooknet-design/src/lib/rehype-ascii.ts b/ooknet-design/src/lib/rehype-
ascii.ts
index 4e8152a..968505e 100644
--- a/ooknet-design/src/lib/rehype-ascii.ts
+++ b/ooknet-design/src/lib/rehype-ascii.ts
@@ -56,13 +56,24 @@ const copyBtn: Element = {
 
 // ---------------------------------------------------------------- code
 
+/** Line class for ```diff fences; null renders as plain code. */
+export function diffLineClass(line: string): string | null {
+  if (line.startsWith("+++") || line.startsWith("---") || line.startsWith("diff "
) || line.startsWith("index ")) return "diff-meta";
+  if (line.startsWith("@@")) return "diff-hunk";
+  if (line.startsWith("+")) return "diff-add";
+  if (line.startsWith("-")) return "diff-del";
+  return null;
+}
+
 function buildBoxChildren(text: string, lang: string, frameW: number): ElementCon
tent[] {
   const inner = frameW - 4;
   const lines = text.replace(/\n+$/, "").split("\n").flatMap((line) => {
+    // Classified before wrapping so continuation chunks keep the class.
+    const cls = lang === "diff" ? diffLineClass(line) : null;
     const cps = [...line];
-    if (cps.length <= inner) return [line];
-    const chunks: string[] = [];
-    for (let i = 0; i < cps.length; i += inner) chunks.push(cps.slice(i, i + inne
r).join(""));
+    if (cps.length <= inner) return [{ text: line, cls }];
+    const chunks: { text: string; cls: string | null }[] = [];
+    for (let i = 0; i < cps.length; i += inner) chunks.push({ text: cps.slice(i, 
i + inner).join(""), cls });
     return chunks;
   });
 
@@ -80,22 +91,31 @@ function buildBoxChildren(text: string, lang: string, frameW: 
number): ElementCo
     { type: "text", value: "\n" },
   ];
 
-  for (const line of lines) {
+  for (const { text: line, cls } of lines) {
     checkGrid(line, inner, "code block");
     const padLen = Math.max(0, inner - len(line));
     children.push(frame(`${G.V} `));
-    let last = 0;
-    for (const m of line.matchAll(FN_RE)) {
-      if (m.index! > last) children.push({ type: "text", value: line.slice(last, 
m.index) });
+    if (cls) {
       children.push({
         type: "element",
-        tagName: "strong",
-        properties: {},
-        children: [{ type: "text", value: m[0] }],
+        tagName: "span",
+        properties: { className: [cls] },
+        children: [{ type: "text", value: line }],
       });
-      last = m.index! + m[0].length;
+    } else {
+      let last = 0;
+      for (const m of line.matchAll(FN_RE)) {
+        if (m.index! > last) children.push({ type: "text", value: line.slice(last
, m.index) });
+        children.push({
+          type: "element",
+          tagName: "strong",
+          properties: {},
+          children: [{ type: "text", value: m[0] }],
+        });
+        last = m.index! + m[0].length;
+      }
+      if (last < line.length) children.push({ type: "text", value: line.slice(las
t) });
     }
-    if (last < line.length) children.push({ type: "text", value: line.slice(last)
 });
     children.push(frame(`${" ".repeat(padLen)} ${G.V}`));
     children.push({ type: "text", value: "\n" });
   }

diff --git a/ooknet-design/src/lib/rehype-as
cii.ts b/ooknet-design/src/lib/rehype-ascii.
ts
index 4e8152a..968505e 100644
--- a/ooknet-design/src/lib/rehype-ascii.ts
+++ b/ooknet-design/src/lib/rehype-ascii.ts
@@ -56,13 +56,24 @@ const copyBtn: Element =
 {
 
 // ----------------------------------------
------------------------ code
 
+/** Line class for ```diff fences; null ren
ders as plain code. */
+export function diffLineClass(line: string)
: string | null {
+  if (line.startsWith("+++") || line.starts
With("---") || line.startsWith("diff ") || l
ine.startsWith("index ")) return "diff-meta"
;
+  if (line.startsWith("@@")) return "diff-h
unk";
+  if (line.startsWith("+")) return "diff-ad
d";
+  if (line.startsWith("-")) return "diff-de
l";
+  return null;
+}
+
 function buildBoxChildren(text: string, lan
g: string, frameW: number): ElementContent[]
 {
   const inner = frameW - 4;
   const lines = text.replace(/\n+$/, "").sp
lit("\n").flatMap((line) => {
+    // Classified before wrapping so contin
uation chunks keep the class.
+    const cls = lang === "diff" ? diffLineC
lass(line) : null;
     const cps = [...line];
-    if (cps.length <= inner) return [line];
-    const chunks: string[] = [];
-    for (let i = 0; i < cps.length; i += in
ner) chunks.push(cps.slice(i, i + inner).joi
n(""));
+    if (cps.length <= inner) return [{ text
: line, cls }];
+    const chunks: { text: string; cls: stri
ng | null }[] = [];
+    for (let i = 0; i < cps.length; i += in
ner) chunks.push({ text: cps.slice(i, i + in
ner).join(""), cls });
     return chunks;
   });
 
@@ -80,22 +91,31 @@ function buildBoxChildre
n(text: string, lang: string, frameW: number
): ElementCo
     { type: "text", value: "\n" },
   ];
 
-  for (const line of lines) {
+  for (const { text: line, cls } of lines) 
{
     checkGrid(line, inner, "code block");
     const padLen = Math.max(0, inner - len(
line));
     children.push(frame(`${G.V} `));
-    let last = 0;
-    for (const m of line.matchAll(FN_RE)) {
-      if (m.index! > last) children.push({ 
type: "text", value: line.slice(last, m.inde
x) });
+    if (cls) {
       children.push({
         type: "element",
-        tagName: "strong",
-        properties: {},
-        children: [{ type: "text", value: m
[0] }],
+        tagName: "span",
+        properties: { className: [cls] },
+        children: [{ type: "text", value: l
ine }],
       });
-      last = m.index! + m[0].length;
+    } else {
+      let last = 0;
+      for (const m of line.matchAll(FN_RE))
 {
+        if (m.index! > last) children.push(
{ type: "text", value: line.slice(last, m.in
dex) });
+        children.push({
+          type: "element",
+          tagName: "strong",
+          properties: {},
+          children: [{ type: "text", value:
 m[0] }],
+        });
+        last = m.index! + m[0].length;
+      }
+      if (last < line.length) children.push
({ type: "text", value: line.slice(last) });
     }
-    if (last < line.length) children.push({
 type: "text", value: line.slice(last) });
     children.push(frame(`${" ".repeat(padLe
n)} ${G.V}`));
     children.push({ type: "text", value: "\
n" });
   }
 

diff --git a/ooknet-design/src/pages/components.astro b/ooknet-design/src/pages/co
mponents.astro
index 106ca2c..6f54486 100644
--- a/ooknet-design/src/pages/components.astro
+++ b/ooknet-design/src/pages/components.astro
@@ -15,6 +15,10 @@ import type { TreeNode } from "../components/Tree/Tree";
 import Progress from "../components/Progress/Progress.astro";
 import Spinner from "../components/Spinner/Spinner.astro";
 import Status from "../components/Status/Status.astro";
+import TableOfContents from "../components/TableOfContents/TableOfContents.astro"
;
+import Accordion from "../components/Accordion/Accordion.astro";
+import Timeline from "../components/Timeline/Timeline.astro";
+import type { TimelineEntry } from "../components/Timeline/Timeline";
 import { rule, ruleD, row2, pad } from "../lib/ascii";
 
 const section = (title: string, right: string) => (w: number) =>
@@ -37,6 +41,22 @@ const REPORT_FIELDS: Field[] = [
   { kind: "buttons", buttons: [{ label: "TRANSMIT", variant: "primary" }, { label
: "CLEAR", type: "reset" }] },
 ];
 
+const DEMO_HEADINGS = [
+  { depth: 1, slug: "buttons", text: "Buttons" },
+  { depth: 1, slug: "text-fields", text: "Text fields" },
+  { depth: 1, slug: "toggles", text: "Toggles" },
+  { depth: 2, slug: "checkboxes", text: "Checkboxes" },
+  { depth: 2, slug: "radios", text: "Radios" },
+  { depth: 1, slug: "form-frame", text: "Form frame" },
+];
+
+const HISTORY: TimelineEntry[] = [
+  { date: "2026-07-16", title: "N0 STYLE SHEET FILED", desc: "exercises every con
struct the pipeline renders", href: "/notes/n0-style-sheet/" },
+  { date: "2026-04-27", title: "N219 FILED", desc: "using AI to troubleshoot and 
resolve issues", href: "/notes/n219-using-ai-to-troubleshoot/" },
+  { date: "2026-04-25", title: "N218 FILED", href: "/notes/collect-data-to-analyz
e-and-troubleshoot-performance-scenarios/" },
+  { date: "2026-04-19", title: "N217 FILED", desc: "entropy bounds in sparse grap
hs", href: "/notes/n217-entropy-bounds/" },
+];
+
 const FILED_TREE: TreeNode[] = [
   {
     label: "/INFO",
@@ -117,5 +137,40 @@ const FILED_TREE: TreeNode[] = [
   <Pre><Spinner />{" SYNCING INDEX"}</Pre>
   <Pre>{" "}</Pre>
 
+  <Frame build={section("TABLE OF CONTENTS", "TableOfContents.astro")} />
+  <TableOfContents headings={DEMO_HEADINGS} />
+  <Pre>{" "}</Pre>
+
+  <Frame build={section("ACCORDION", "Accordion.astro")} />
+  <Pre>{" "}</Pre>
+  <Accordion label="APPENDIX A - CELL MEASUREMENTS">
+    <p>Cascadia Mono "0" advance at 13px: 7.617px. IBM Plex Mono drifts on box-dr
awing glyphs, which is why it never draws frames.</p>
+  </Accordion>
+  <Accordion label="APPENDIX B - DROPPED GLYPH SETS">
+    <p>Heavy box-drawing (U+250F etc.) fell back to a wider face and bent every b
order it touched. Dropped from the grid.</p>
+  </Accordion>
+  <Pre>{" "}</Pre>
+
+  <Frame build={section("TIMELINE", "Timeline.astro")} />
+  <Pre>{" "}</Pre>
+  <Timeline entries={HISTORY} />
+  <Pre>{" "}</Pre>
+
+  <Frame build={section("TOAST", "Toast.astro")} />
+  <Pre>{" "}</Pre>
+  <Pre><span id="toast-ok"><Button label="QUEUE" variant="primary" /></span>{"  "
}<span id="toast-error"><Button label="JAM" /></span>{"  "}<span id="toast-pending
"><Button label="HOLD" /></span></Pre>
+  <Pre>{" "}</Pre>
+
   <Frame build={(w) => rule(w)} />
 </ArticleLayout>
+
+<script>
+  import type { StatusVariant } from "../components/Status/Status";
+
+  const wire = (id: string, message: string, variant: StatusVariant) =>
+    document.querySelector(`#${id} .btn`)?.addEventListener("click", () => window
.ooknetToast?.(message, variant));
+
+  wire("toast-ok", "TRANSMISSION QUEUED", "ok");
+  wire("toast-error", "FREQUENCY OUT OF BAND", "error");
+  wire("toast-pending", "AWAITING PEER CHECK", "pending");
+</script>

diff --git a/ooknet-design/src/pages/compone
nts.astro b/ooknet-design/src/pages/componen
ts.astro
index 106ca2c..6f54486 100644
--- a/ooknet-design/src/pages/components.ast
ro
+++ b/ooknet-design/src/pages/components.ast
ro
@@ -15,6 +15,10 @@ import type { TreeNode } 
from "../components/Tree/Tree";
 import Progress from "../components/Progres
s/Progress.astro";
 import Spinner from "../components/Spinner/
Spinner.astro";
 import Status from "../components/Status/St
atus.astro";
+import TableOfContents from "../components/
TableOfContents/TableOfContents.astro";
+import Accordion from "../components/Accord
ion/Accordion.astro";
+import Timeline from "../components/Timelin
e/Timeline.astro";
+import type { TimelineEntry } from "../comp
onents/Timeline/Timeline";
 import { rule, ruleD, row2, pad } from "../
lib/ascii";
 
 const section = (title: string, right: stri
ng) => (w: number) =>
@@ -37,6 +41,22 @@ const REPORT_FIELDS: Fiel
d[] = [
   { kind: "buttons", buttons: [{ label: "TR
ANSMIT", variant: "primary" }, { label: "CLE
AR", type: "reset" }] },
 ];
 
+const DEMO_HEADINGS = [
+  { depth: 1, slug: "buttons", text: "Butto
ns" },
+  { depth: 1, slug: "text-fields", text: "T
ext fields" },
+  { depth: 1, slug: "toggles", text: "Toggl
es" },
+  { depth: 2, slug: "checkboxes", text: "Ch
eckboxes" },
+  { depth: 2, slug: "radios", text: "Radios
" },
+  { depth: 1, slug: "form-frame", text: "Fo
rm frame" },
+];
+
+const HISTORY: TimelineEntry[] = [
+  { date: "2026-07-16", title: "N0 STYLE SH
EET FILED", desc: "exercises every construct
 the pipeline renders", href: "/notes/n0-sty
le-sheet/" },
+  { date: "2026-04-27", title: "N219 FILED"
, desc: "using AI to troubleshoot and resolv
e issues", href: "/notes/n219-using-ai-to-tr
oubleshoot/" },
+  { date: "2026-04-25", title: "N218 FILED"
, href: "/notes/collect-data-to-analyze-and-
troubleshoot-performance-scenarios/" },
+  { date: "2026-04-19", title: "N217 FILED"
, desc: "entropy bounds in sparse graphs", h
ref: "/notes/n217-entropy-bounds/" },
+];
+
 const FILED_TREE: TreeNode[] = [
   {
     label: "/INFO",
@@ -117,5 +137,40 @@ const FILED_TREE: TreeN
ode[] = [
   <Pre><Spinner />{" SYNCING INDEX"}</Pre>
   <Pre>{" "}</Pre>
 
+  <Frame build={section("TABLE OF CONTENTS"
, "TableOfContents.astro")} />
+  <TableOfContents headings={DEMO_HEADINGS}
 />
+  <Pre>{" "}</Pre>
+
+  <Frame build={section("ACCORDION", "Accor
dion.astro")} />
+  <Pre>{" "}</Pre>
+  <Accordion label="APPENDIX A - CELL MEASU
REMENTS">
+    <p>Cascadia Mono "0" advance at 13px: 7
.617px. IBM Plex Mono drifts on box-drawing 
glyphs, which is why it never draws frames.<
/p>
+  </Accordion>
+  <Accordion label="APPENDIX B - DROPPED GL
YPH SETS">
+    <p>Heavy box-drawing (U+250F etc.) fell
 back to a wider face and bent every border 
it touched. Dropped from the grid.</p>
+  </Accordion>
+  <Pre>{" "}</Pre>
+
+  <Frame build={section("TIMELINE", "Timeli
ne.astro")} />
+  <Pre>{" "}</Pre>
+  <Timeline entries={HISTORY} />
+  <Pre>{" "}</Pre>
+
+  <Frame build={section("TOAST", "Toast.ast
ro")} />
+  <Pre>{" "}</Pre>
+  <Pre><span id="toast-ok"><Button label="Q
UEUE" variant="primary" /></span>{"  "}<span
 id="toast-error"><Button label="JAM" /></sp
an>{"  "}<span id="toast-pending"><Button la
bel="HOLD" /></span></Pre>
+  <Pre>{" "}</Pre>
+
   <Frame build={(w) => rule(w)} />
 </ArticleLayout>
+
+<script>
+  import type { StatusVariant } from "../co
mponents/Status/Status";
+
+  const wire = (id: string, message: string
, variant: StatusVariant) =>
+    document.querySelector(`#${id} .btn`)?.
addEventListener("click", () => window.ookne
tToast?.(message, variant));
+
+  wire("toast-ok", "TRANSMISSION QUEUED", "
ok");
+  wire("toast-error", "FREQUENCY OUT OF BAN
D", "error");
+  wire("toast-pending", "AWAITING PEER CHEC
K", "pending");
+</script>
 

diff --git a/ooknet-design/src/pages/notes/[slug].astro b/ooknet-design/src/pages/
notes/[slug].astro
index 0530758..f039af8 100644
--- a/ooknet-design/src/pages/notes/[slug].astro
+++ b/ooknet-design/src/pages/notes/[slug].astro
@@ -5,6 +5,7 @@ import TopHeader from "../../components/TopHeader/TopHeader.astro"
;
 import ArticleHeader from "../../components/ArticleHeader/ArticleHeader.astro";
 import DocumentTitle from "../../components/DocumentTitle/DocumentTitle.astro";
 import ArticleMeta from "../../components/ArticleMeta/ArticleMeta.astro";
+import TableOfContents from "../../components/TableOfContents/TableOfContents.ast
ro";
 import ArticleSeeAlso from "../../components/ArticleSeeAlso/ArticleSeeAlso.astro"
;
 import ArticleTags from "../../components/ArticleTags/ArticleTags.astro";
 import ArticlePagination from "../../components/ArticlePagination/ArticlePaginati
on.astro";
@@ -30,13 +31,14 @@ interface Props {
   next: PageLink | null;
 }
 const { note, prev, next } = Astro.props as Props;
-const { Content } = await render(note);
+const { Content, headings } = await render(note);
 ---
 <ArticleLayout title={note.data.title}>
   <TopHeader />
   <ArticleHeader fm={note.data} />
   <DocumentTitle fm={note.data} />
   <ArticleMeta fm={note.data} path={`notes/${note.id}`} />
+  {headings.length >= 3 && <TableOfContents headings={headings} />}
   <Prose><Content /></Prose>
   <ArticleSeeAlso fm={note.data} />
   <ArticleTags fm={note.data} />

diff --git a/ooknet-design/src/pages/notes/[
slug].astro b/ooknet-design/src/pages/notes/
[slug].astro
index 0530758..f039af8 100644
--- a/ooknet-design/src/pages/notes/[slug].a
stro
+++ b/ooknet-design/src/pages/notes/[slug].a
stro
@@ -5,6 +5,7 @@ import TopHeader from "../..
/components/TopHeader/TopHeader.astro";
 import ArticleHeader from "../../components
/ArticleHeader/ArticleHeader.astro";
 import DocumentTitle from "../../components
/DocumentTitle/DocumentTitle.astro";
 import ArticleMeta from "../../components/A
rticleMeta/ArticleMeta.astro";
+import TableOfContents from "../../componen
ts/TableOfContents/TableOfContents.astro";
 import ArticleSeeAlso from "../../component
s/ArticleSeeAlso/ArticleSeeAlso.astro";
 import ArticleTags from "../../components/A
rticleTags/ArticleTags.astro";
 import ArticlePagination from "../../compon
ents/ArticlePagination/ArticlePagination.ast
ro";
@@ -30,13 +31,14 @@ interface Props {
   next: PageLink | null;
 }
 const { note, prev, next } = Astro.props as
 Props;
-const { Content } = await render(note);
+const { Content, headings } = await render(
note);
 ---
 <ArticleLayout title={note.data.title}>
   <TopHeader />
   <ArticleHeader fm={note.data} />
   <DocumentTitle fm={note.data} />
   <ArticleMeta fm={note.data} path={`notes/
${note.id}`} />
+  {headings.length >= 3 && <TableOfContents
 headings={headings} />}
   <Prose><Content /></Prose>
   <ArticleSeeAlso fm={note.data} />
   <ArticleTags fm={note.data} />
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET