# 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
