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

298 lines
9.8 KiB
Markdown

# 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:**
```toon
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):
```yaml
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.
```html
<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:**
```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:**
```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.
```html
<!-- 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:**
```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:**
```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.
```html
<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:**
```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.
```html
<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
```html
<nav>
<button class="active [color-class]" onclick="go('[id]',this,'[color]')">[label]</button>
<!-- one per tab -->
</nav>
```
```js
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:
```html
<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