# Bramwell rendering spec (v1)

How to render an IQ Apparatus Bramwell Method activity on a host site so the
learner experience matches IQ Apparatus field for field.

Applies to any `GET /api-course-content` response where `data.format === "bramwell"`.

---

## 1. Payload fields you must consume

| Field | Purpose |
| --- | --- |
| `format` | `"bramwell"` → use this spec. `null` → standard CE renderer. |
| `delivery_style` | Optional hook label shown above the activity title. |
| `learning_objectives[]` | Landing screen + certificate. |
| `accreditation` | Credit designation, provider statement, release/expiration dates. |
| `landing_page` | Statement of need, target audience, how to earn credit, disclosure summary, hardware requirements. |
| `confidence_baseline` | Pre-activity confidence slider (`prompt`, `min`, `max`, `minLabel`, `maxLabel`). |
| `phases[]` | Ordered screens. Each has `id`, `title`, `type`, `required`, `order`, `components[]`. |
| `phases[].debrief_readout` | Matched pre/post items for the knowledge-lift card. Labels only — no answer keys. |
| `debrief` | The takeaway card: `caseTakeaway`, `answerKeyHtml`, `nextStepsHtml`. |
| `references[]` | Numbered citations backing `[N]` markers. |
| `series` | Series roadmap: `name`, `position`, `total`, `modules[]`. |
| `media_expires_at` | Payload-level expiry for signed media. |
| `<video block>.expires_at` | Per-block expiry for that asset's signed URL. |

## 2. Phase order and what each screen shows

Render `phases` in the order returned. Do not reorder or merge.

1. **Hook / case opening** — title, `delivery_style` chip, opening content, avatar video.
2. **Pre-assessment** — pre-test questions plus the `confidence_baseline` slider.
3. **Decision path** — the core Bramwell loop: content, `choice` decision cards, `reflection`, `image-hotspot`.
4. **Post-assessment** — post-test questions; on results, the takeaway card renders **above** per-question review.
5. **Evaluation** — `evaluation_questions` verbatim, including the commit-to-change item.

Series modules also show a roadmap: "Module `series.position` of `series.total`".

## 3. Component render contract

| Type | Required rendering | Accepted fallback |
| --- | --- | --- |
| `talking-avatar`, `ai-video` | `<video>` with `poster_url`, captions from `caption_url`. Show a "render pending" state when `video_status !== "ready"`. | Poster image + transcript, if provided. |
| `choice` (decision card) | Card list, single select, submit action. No rationale before submission. | Plain radio group. |
| `reflection` | Free-text prompt, stored and posted with the attempt. | Textarea. |
| `confidence-check` | Slider from `min` to `max` with end labels. **Always a slider — a confidence-check never carries `options`; treat a payload that does as malformed.** | Radio scale with the same integer values. |
| `image-hotspot` | Image with clickable regions from `hotspots[]`. | Static labeled image + the prompt + a single-select list of hotspot labels. |
| `card-flip` | Flip cards. | Two-column term/definition list. |
| `drag-drop-match` | Drag sources onto targets. | Per-source dropdown of target labels. |
| `evaluation` | Types as delivered (`single-select`, `scale`, `text`). | Native form controls. |
| Rich content (`content`, HTML strings) | Wrap in `.bw-prose` inside `.iq-bramwell-theme`. | — |

**Every fallback must post the same answer shape to `POST /api-submit-progress`.**
Outcomes and Moore's-level reporting depend on answer shape, not on widget fidelity.

## 4. Citations

Content HTML contains bracketed markers `[1]`, `[2][3]`. Linkify each marker to
the matching `references[].number` entry and render the reference list numbered.
Never convert markers to superscript.

## 4a. Decision gating (contract 2026-08-28)

Render `components` in the order returned — never hoist a block above a
decision. Visibility, however, is gated:

| Field | Meaning |
| --- | --- |
| `reveals_on_answer: true` | This component is a gate anchor — a decision whose answer unlocks what follows. Anchors are never gated themselves. |
| `reveal_after: <component id> \| null` | Hold this component until the referenced decision has been answered and its rationale revealed. `null` renders immediately. |

Both fields ship on every delivered component. `id` is always present
(synthesized when the authoring record lacked one).

On a Bramwell decision phase the authored order is typically:

```text
rich-text instruction → choice (decision) → video (outcome) → rich-text (teach-back)
→ reflection → confidence-check
```

Everything after the `choice` carries `reveal_after: <choice id>`, because the
outcome video and teach-back debrief state the correct answer. Showing them
up-front spoils the decision. This mirrors the IQ Apparatus learner player,
where post-decision blocks live on later steps that cannot be reached until the
question is answered.

Gating is a **visibility** rule, not a reorder: once revealed, the block appears
in its authored position. Rationale still comes only from the source the
grading mode dictates — locally for `client_graded: true` decisions, from
`POST /api-submit-progress` for `assessmentMode` items. Never compute or
fabricate it.

## 5. State ownership

**Host owns:**
- Phase progression, step navigation, "locked until previous phase complete"
  (drive from `phases[].required` and `order`).
- Persisting in-progress answers locally between submissions.

**Server owns (never compute locally):**
- Answer correctness, rationale, and feedback — returned per question by
  `POST /api-submit-progress` after submission. The course payload contains no
  answer keys by design.
- Score, pass/fail, completion, credit, and certificate eligibility — via the
  progress and completions endpoints.

Do not gate the UI on locally derived correctness, and do not fabricate feedback text.

## 6. Media freshness

`video_url` values are short-lived signed URLs (about six hours). Never cache or
static-render them past `expires_at` (per block) or `media_expires_at` (payload).
Re-fetch the course to mint fresh URLs immediately before playback.

## 7. Styling

Link the public theme and wrap the activity:

```html
<link rel="stylesheet" href="https://iqapparatus.com/bramwell-theme.css">
<div class="iq-bramwell-theme"> <!-- activity --> </div>
```

All custom properties and recipes (`.bw-card`, `.bw-panel`, `.bw-chip`,
`.bw-micro`, `.bw-choice`, `.bw-prose`, `.bw-references`, `.bw-mark`) are scoped
under `.iq-bramwell-theme`. Nothing is declared on `:root`, so the sheet cannot
leak into the host layout. Load Space Grotesk (display) and Poppins (body), or
override `--bw-font-display` / `--bw-font-body` inside the wrapper.

## 8. Catalog cards (contract 2026-08-20)

`GET /api-courses` returns everything a card needs pre-derived in
`card_display_config` — do not re-derive credits, price, or CTA wording.

| Card element | Field |
| --- | --- |
| Image (16:10, `object-fit: cover`) | `card_display_config.image.url` / `.alt` |
| Corner ribbon | `card_display_config.stripe.text` + `.variant` |
| Badge | `card_display_config.badge.text` + `.style` |
| Provider line | `card_display_config.provider_name` |
| Title | `name` |
| Summary (2 lines, plain text) | `card_display_config.summary` |
| Credit chips | `card_display_config.credit_chips[].display` |
| Format line chip | `card_display_config.format_line` |
| Price | `card_display_config.price.display` |
| Button | `card_display_config.cta.label` + `.style` |

Rules:

- Chips are already merged by amount (`0.25 CME · NCPD` is one chip, not two).
  Suppress the whole chip row when `hide_credit_chips` is true.
- `price.display` is authoritative (`Free`, `$49`, `$49.50`). Never format
  `marketplace_price` yourself.
- `cta` is the guest-state label. Once your host knows the learner is enrolled,
  swap in your own resume/continue label — that state is yours, not ours.
- Cards are equal-height: pin the price/CTA footer with `margin-top: auto`.

Markup recipe (styles ship in the theme sheet):

```html
<div class="iq-bramwell-theme">
  <div class="bw-catalog-grid">
    <article class="bw-catalog-card">
      <div class="bw-catalog-card__media">
        <span class="bw-catalog-card__stripe" data-variant="new">New</span>
        <img src="…" alt="…">
      </div>
      <div class="bw-catalog-card__body">
        <p class="bw-catalog-card__provider">Oncology Data Advisor</p>
        <h3 class="bw-catalog-card__title">…</h3>
        <p class="bw-catalog-card__summary">…</p>
        <div class="bw-chip-row">
          <span class="bw-chip">0.25 CME · NCPD</span>
          <span class="bw-chip bw-chip--format">15-minute case decision</span>
        </div>
        <div class="bw-catalog-card__footer">
          <span class="bw-catalog-card__price">Free</span>
          <button class="bw-cta" data-style="primary">Start learning</button>
        </div>
      </div>
    </article>
  </div>
</div>
```

## 9. Series rail

Catalog items that belong to a microlearning series carry a `series` object
(`id`, `name`, `position`, `total`, `is_series_module: true`). Group items by
`series.id`, sort by `series.position`, and render one series card per group
instead of N loose module cards.

- `total` is the full published roster, so `total - modules.length` modules are
  "coming soon" — show the count, don't hide it.
- Sum `credit_designations[].credit_hours` across the group for total credit;
  minutes ≈ hours × 60.
- Price line: "Free" when every module is free, otherwise "From $X" using the
  lowest paid module.
- Sequential gating is yours (`sequential_gating_enforced: false`): the API
  serves module N regardless of N-1, so lock modules from your own completion
  records.
- Recipe classes: `.bw-series-card`, `__eyebrow`, `__title`, `__meta`,
  `__modules` with `.bw-series-module` / `.bw-series-module__index`, `__footer`.
  Use `aria-disabled="true"` on locked modules.

The `SeriesCardView` interface in the contract module documents the exact
derived shape.

### Series object fields (contract 2026-08-21)

`series` on course content carries: `series_id`, `name`, `description`,
`position`, `total`, `modules[]`, `next`, `previous`,
`sequential_gating_enforced`, and — since 2026-08-21 — `course_id`,
`activity_type`, `delivery_format` (the current module's identity, mirroring
the top-level fields of the same names).

`sequential_gating_enforced` is **advisory and always `false`**. It is not a
server-side guarantee: the delivery API will return module N to any authorized
caller even when module N−1 has never been started. Enforce ordering from your
own completion records.

### Introduction phase

`introduction` is a declared phase type (added 2026-08-20) and sits first in
`PHASE_CANONICAL_ORDER`. It carries only its own activity content — case setup,
"meet the patient", framing. It never restates `landing_page`,
`learning_objectives`, or `accreditation`; that material lives only in the
top-level landing fields, so render both without deduplication logic.

### Fetching the contract module

`/contract/*.ts` is served as `text/plain; charset=utf-8` with open CORS. If your
host or tooling still objects to the extension, fetch the identical plain-text
mirrors: `/contract/iq-render-contract.ts.txt` and
`/contract/iq-render-contract-<revision>.ts.txt`.


## 11. Rendered geometry (contract 2026-08-26)

Styling parity is verifiable, not a matter of opinion. Two artifacts let you
measure your render against ours without access to a running IQ instance.

**Reference render** — `https://iqapparatus.com/reference/bramwell`

Unauthenticated, no database reads, driven by a frozen fixture through the same
Bramwell primitives the learner sees. Every canonical surface carries a stable
`data-ref` attribute:

`phase-frame`, `step-chip`, `heading-ramp`, `decision-card`, `answer-row`,
`reveal-panel`, `confidence-slider`, `reflection-field`, `feedback-correct`,
`feedback-incorrect`, `takeaway-card`, `series-rail`, `catalog-card`,
`action-bar`.

Query controls:

| Param | Values | Effect |
| --- | --- | --- |
| `surface` | any `data-ref` id | Render that surface alone. |
| `state` | `resting`, `selected`, `correct`, `incorrect` | Interaction state. |
| `w` | px | Fixed content width, independent of viewport. |
| `chrome` | `0` | Strip page chrome — surfaces only, for pixel capture. |

**Geometry snapshot** — `https://iqapparatus.com/contract/bramwell-geometry.json`
(plain-text mirror `.json.txt`)

Computed geometry of every surface at 375 / 768 / 1280 in each state: width,
height, padding, border radius, border width, gap, font family, font size,
font weight, line height, color, background. Captured in headless Chromium at
device scale factor 1. Where a surface wraps a painted card, the numbers
describe the painted element, and `first_button` / `first_heading` describe its
primary control and heading.

Diff recipe:

1. Render your own surface with the same fixture content, wrapped in
   `.iq-bramwell-theme` with `bramwell-theme.css` linked.
2. Read the same computed properties at the same three widths.
3. Compare against the snapshot keyed by `surfaces[<data-ref>][<width>][<state>]`.

Tolerance: exact match on padding, radius, border width, font size, font
weight, and line height. Width and height may differ where your container
width differs — normalize by setting the same content width first. Colors must
match exactly; a mismatch means the theme sheet was not applied or was
overridden by host CSS.

The snapshot is regenerated whenever the primitives change and is republished
with the revision that changed them. Pin the revision you tested against.


## 12. Navigation microcopy (recommended, host-owned)

Navigation and submission button text is **not** in the payload. There is no
`next_label` or `submit_label` field, and there never will be — these strings
belong to your host so you can localize, shorten, or rebrand them freely.

For hosts that want to match IQ Apparatus word for word, this is the convention
our own player follows:

| Situation | Label |
| --- | --- |
| Move backward, anywhere | `Back` |
| Advance **within** a phase (block 2 of 5, question 2 of 4) | `Next` |
| Advance **out of** a phase into the next one | `Continue` |
| Commit one graded item | `Submit answer` |
| Confirm a graded batch (pre-test) | `Submit answers` |
| Confirm a graded batch (post-test) | `Submit assessment` |
| Ungraded free-text save (reflection) | `Save reflection` → `Saved` |
| Pending save | `Saving…` |
| Pending submission | `Submitting…` |

Rules that carry the meaning, if you localize:

- Sentence case for every control. Only proper nouns and product section names
  are capitalized.
- `Next` and `Continue` are not interchangeable. `Next` keeps the learner inside
  the current phase; `Continue` leaves it. Collapsing both into one word removes
  the learner's only cue about how much of the phase remains.
- Reserve `Submit` wording for actions that record a graded or evaluative
  response the learner cannot silently redo. Everything else advances or saves.
- Use the single-character ellipsis `…` in pending states, not three periods.

Enrolled-state resume copy (see §8) is likewise yours — pick one label and use
it consistently with the table above.


## 10. Conformance checklist


Test against one published module before going live:

- [ ] All phases present, in the returned order.
- [ ] Avatar video plays; a pending render shows a graceful state.
- [ ] Decision cards reveal **no** rationale before submission.
- [ ] Blocks with a non-null `reveal_after` (outcome video, teach-back debrief,
      reflection, confidence-check) are hidden until the decision is answered.
- [ ] `confidence-check` renders as a slider, never an option list.
- [ ] Post-test results show the takeaway card above per-question review.
- [ ] `[N]` markers link to a numbered reference list.
- [ ] Series modules display "Module N of M".
- [ ] Evaluation shows the four ACCME items plus commit-to-change.
- [ ] Confidence baseline captured pre-activity and reflected in the knowledge-lift readout.
- [ ] Accreditation statement and credit hours shown on the landing screen.
- [ ] No `video_url` is cached beyond its `expires_at`.
- [ ] Catalog cards render credit chips, price, and CTA from
      `card_display_config` with no local derivation.
- [ ] Series modules are grouped into one series card with "N modules ·
      M available now".
- [ ] Geometry diffed against `bramwell-geometry.json` at 375 / 768 / 1280
      with zero delta on padding, radius, border, and type.
- [ ] Navigation labels follow §12 (or a deliberate, consistent host override):
      `Next` inside a phase, `Continue` between phases, `Submit …` only for
      graded commits.

