Files
singular-particular-space/skills/annotated-writing/annotated-writing-build.md
JL Kruger 5422131782 Initial commit — Singular Particular Space v1
Homepage (site/index.html): integration-v14 promoted, Writings section
integrated with 33 pieces clustered by type (stories/essays/miscellany),
Writings welcome lightbox, content frame at 98% opacity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 12:09:22 +02:00

9.8 KiB

Annotated Writing Build

Instructions for a build agent. Input: a completed content map .md file. Output: a single-file .html artefact. Do not perform literary analysis. Do not rewrite or improve the source text. Execute the map.


Two-Agent Protocol

This agent receives a content map. It does not produce content maps. If asked to do both in one context, refuse the analysis task and request the content map as input.

What the build agent reads from the content map:

  • Tab definitions (TOON)
  • Decoder metadata (TOON) + decoder bodies (YAML)
  • Lightbox definitions (YAML)
  • Accordion content (YAML)
  • Bias notes (YAML)
  • Further reading (TOON)
  • Annotated source text (with [DECODER], [LIGHTBOX], [ORDER], [LETTER-*] markers)

Reading TOON in the Content Map

Content maps use TOON for uniform arrays. Parse as follows:

Tabular array syntax:

arrayName[N]{field1,field2,field3}:
  value1,value2,value3
  value1,value2,value3

[N] is the declared row count — validate against actual rows. {fields} is the column header — same for every row. Values follow CSV quoting rules: strings containing commas are double-quoted.

YAML scalar syntax (for body text — not TOON):

key: >
  Block scalar. Paragraph text. May contain commas freely.
  Blank line = new paragraph in rendered output.

Both may appear in the same content map. TOON for metadata; YAML for prose.


Aesthetic Direction

The aesthetic is set per-project by the user. The content map may include an aesthetic: key. If absent, infer from context clues in the source text. When in doubt, ask before building.

Regardless of aesthetic, these rules apply:

  • Choose fonts that are distinctive and specific to the aesthetic. Never: Arial, Inter, Roboto, system-ui.
  • Use CSS custom properties for all colors. No hardcoded hex values in component CSS.
  • Dark or light themes are both valid. Commit to one — do not default to white-on-grey.
  • Background texture adds atmosphere. Scanlines, grain, parchment patterns, noise — pick one and use it subtly.
  • Tab navigation is sticky. One tab visible at a time via JS class toggle.

Component Library

All components must be implemented as specified. Do not substitute or simplify.


Decoder

Inline interactive element. Wraps a phrase in prose. Floating panel on click.

Critical structural rule: Every element inside a decoder must be a <span>, never a <div>. Decoders live inside <p> tags. A <div> inside a <p> silently ejects the panel from the DOM. Use <span> with display: block for block-like rendering.

<span class="dc" id="[id]">
  <button class="dc-btn [color-class]" onclick="tog('[id]',this)">[phrase from text]</button>
  <span class="dc-panel [color-class]">
    <span class="d-tag [color-class]">[tag text]</span>
    <span class="d-head">[label]</span>
    [body text]
    <a class="d-link [color-class]" href="[url]" target="_blank">→ [link label]</a>
  </span>
</span>

Required CSS:

.dc { display: inline; position: relative; }
.dc-panel {
  display: none;
  position: absolute;       /* never relative */
  top: calc(100% + 6px);
  left: 0;
  z-index: 500;
  width: 300px;             /* adjust to aesthetic */
}
.dc-panel.open { display: block; }
.dc-panel.flip { left: auto; right: 0; }

Required JS:

function tog(id, btn) {
  const w = document.getElementById(id);
  const p = w.querySelector('.dc-panel');
  const open = p.classList.contains('open');
  document.querySelectorAll('.dc-panel.open').forEach(x => x.classList.remove('open'));
  document.querySelectorAll('.dc-btn.open').forEach(x => x.classList.remove('open'));
  if (!open) {
    p.classList.add('open'); btn.classList.add('open');
    const r = p.getBoundingClientRect();
    p.classList.toggle('flip', r.right > window.innerWidth - 16);
  }
}
document.addEventListener('click', e => {
  if (!e.target.closest('.dc')) {
    document.querySelectorAll('.dc-panel.open').forEach(x => x.classList.remove('open'));
    document.querySelectorAll('.dc-btn.open').forEach(x => x.classList.remove('open'));
  }
});

Trigger visual requirements:

  • Must look clickable (cursor pointer, border-bottom dashed or dotted)
  • Must indicate open/closed state (chevron via ::after, or similar)
  • Color coding must match the scheme defined in the content map

Lightbox

Full-screen overlay. Triggered by an inline button in the prose.

<!-- Trigger — inline in prose -->
<button class="lb-t [color-class]" onclick="lb('[id]')">[trigger phrase]</button>

<!-- Lightbox — at end of body, before script tag -->
<div class="lb-overlay" id="[id]" onclick="closeLbOv(event,'[id]')">
  <div class="lb-box [color-class]">
    <div class="lb-head [color-class]">
      <div>
        <span class="lb-eyebrow [color-class]">[eyebrow]</span>
        <span class="lb-title">[title]</span>
      </div>
      <button class="lb-close" onclick="closeLb('[id]')"></button>
    </div>
    <div class="lb-body">
      <!-- h3 per section, p per paragraph, lb-src link at end -->
    </div>
  </div>
</div>

Required CSS:

.lb-overlay { display: none; position: fixed; inset: 0; z-index: 2000;
  align-items: center; justify-content: center; padding: 20px; }
.lb-overlay.open { display: flex; }
.lb-box { max-height: 88vh; overflow-y: auto; }
.lb-head { position: sticky; top: 0; }

Required JS:

function lb(id) { document.getElementById(id).classList.add('open'); document.body.style.overflow = 'hidden'; }
function closeLb(id) { document.getElementById(id).classList.remove('open'); document.body.style.overflow = ''; }
function closeLbOv(e, id) { if (e.target === document.getElementById(id)) closeLb(id); }
document.addEventListener('keydown', e => {
  if (e.key === 'Escape') {
    document.querySelectorAll('.lb-overlay.open').forEach(x => x.classList.remove('open'));
    document.body.style.overflow = '';
  }
});

Accordion

Expandable sections within educational tabs. One open at a time.

The accordion panel uses <div> — it is not inside prose, so block elements are safe here.

<div class="acc">
  <button class="acc-btn" onclick="acc(this)">
    <span>[heading]</span>
    <span class="acc-arrow [color-class]"></span>
  </button>
  <div class="acc-panel">
    <!-- p tags per paragraph -->
  </div>
</div>

Required JS:

function acc(btn) {
  const p = btn.nextElementSibling;
  const open = btn.classList.contains('open');
  document.querySelectorAll('.acc-btn.open').forEach(b => {
    b.classList.remove('open'); b.nextElementSibling.classList.remove('open');
  });
  if (!open) { btn.classList.add('open'); p.classList.add('open'); }
}

Bias Note

A styled callout block at the top of each analytical tab, before the first accordion.

<div class="bias [color-class]">
  <strong>A note:</strong> [bias note text from content map]
</div>

Not collapsible. Always visible. One per analytical tab.


Tab Navigation

<nav>
  <button class="active [color-class]" onclick="go('[id]',this,'[color]')">[label]</button>
  <!-- one per tab -->
</nav>
function go(id, btn, col) {
  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
  document.querySelectorAll('nav button').forEach(b => { b.classList.remove('active'); /* remove all color classes */ });
  document.getElementById('tab-' + id).classList.add('active');
  btn.classList.add('active', col || 'white');
  window.scrollTo(0, 0);
}

Tab content divs:

<div class="tab [active-on-load]" id="tab-[id]">
  <!-- tab content -->
</div>

Source Text — Markers to HTML

Translate content map markers into HTML components:

Marker Renders as
[DECODER:id] phrase [/DECODER] <span class="dc" id="id"> decoder component
[LIGHTBOX:id] phrase [/LIGHTBOX] <button class="lb-t"> trigger
[ORDER] "text" [/ORDER] Styled display block — dialogue/order separator
[LETTER-START:sender] Opening of styled letter card
[LETTER-END] Close of letter card
[PULL-QUOTE] text [/PULL-QUOTE] Styled pull-quote block
[EDITORIAL] text [/EDITORIAL] Styled editorial aside
[SECTION-BREAK] <hr> with aesthetic treatment

Letter senders each get a distinct visual treatment (border color, label, optional stamp). Define sender colors in CSS variables using the project color scheme.


File Structure

Single .html file. All CSS in <style> in <head>. All JS in <script> immediately before </body>. No external dependencies except Google Fonts (loaded via <link>). All lightboxes placed together at the end of <body>, before <script>. All external links use target="_blank".


Critical Bugs (Permanent Reference)

1. Block elements inside <p> tags silently break the DOM. <div> inside <p> is invalid HTML. The browser auto-closes the <p>, ejecting the decoder panel. Every decoder element must be a <span>.

2. Decoder panel must be position: absolute. position: relative expands the parent span when the panel opens, collapsing the surrounding text. Use position: absolute.

3. Decoder panels clip off the right edge. After opening, check panel.getBoundingClientRect().right > window.innerWidth - 16. If true, add .flip class (left: auto; right: 0).

4. Lightboxes placed inside tab content divs break scroll. Place all lightbox overlays as direct children of <body>, after all tab content, before <script>.


Output Requirements

  • Single .html file
  • Source text reproduced exactly — no edits, no corrections (errors in the original stay)
  • All decoders, lightboxes, accordions, and tabs from the content map implemented
  • Bias notes present on every analytical tab
  • All external links open in new tab
  • Passes a basic sanity check: all decoder IDs referenced in prose exist in the JS/HTML