krueng.ai · page type reference

The Hybrid lesson

A Discussion lesson with a Lab-type instrument console dropped into one of its segments. The reader argues about a contested subject, and the instruments are what they argue with — they operate the evidence rather than being told what it says.

The two parent types: a Discussion lesson (trilingual, evidence and argument, three open questions per segment) and a Lab lesson (bilingual, 5–8 machines the student drives). Read both before building a Hybrid — this page documents only what is different, and assumes the parents for everything else.

This file: HTML/template_hybrid.html · live at krueng.ai/template_hybrid.html.
Reference implementation: HTML/qi.html (2,733 lines, self-contained).
First of the type: HTML/qi.html.
Last updated 2026-08-02.

Contents
  1. When a Hybrid is the right type
  2. Page skeleton
  3. Two themes in one file
  4. The console: tabs and benches
  5. Evidence states, not opinions
  6. SVG traps that cost real debugging
  7. Grounding and fact-checking
  8. The explainer video
  9. What you inherit unchanged
  10. Testing
  11. Build checklist

1 · When a Hybrid is the right type

Reach for it when the subject is contested and instrumentable at the same time. A Discussion alone leaves the reader taking your word for the evidence. A Lab alone teaches a mechanism with nothing at stake. The Hybrid exists for the case where the argument is about what the measurements show, so the reader needs to handle the measurements before the argument means anything.

TypeThe student should…Reference
Laboperate the subjectneuro_lab.html
Discussionargue about the subjectbitcoin.html
Hybridadjudicate — argue using instruments they drove themselvesqi.html
The test for the subject

Two competing claims, each with real evidence, that fail in different places. On qi.html the pairing is qi and negative air ions: one has no unit and no instrument yet produces measurable clinical results, the other has a unit, an instrument and a confirmed mechanism yet fails on benefit. The instruments exist to make that inversion something the reader discovers rather than something they are told.

If both claims fail in the same way, you have a Discussion. If neither is contested, you have a Lab.

2 · Page skeleton

A Discussion skeleton with one section replaced. The console goes in the middle, after both subjects have been introduced and before the synthesis — the reader needs the vocabulary to drive it and the argument needs the reader to have driven it.

<body class="lang-en">
  .langbar                      — EN / 中文 / ไทย / sound, fixed
  .navbtn + nav.toc + .toc-scrim
  .topbtn
  header.hero#intro
  <main>
    section#vocabulary          — 01, always
    section#<subject-a>         — 02, the first claim
    section#<subject-b>         — 03, the second claim
    section#lab                 — 04, THE CONSOLE — the only new part
    section#measured            — 05, what the evidence shows
    section#inversion           — 06, the synthesis, usually a table
    section#<coda>              — 07, the transferable habit
  </main>
  footer                        — sources, label key, what the instruments are NOT
  <script>

Seven sections is the working size. Every one still carries three summaries, a Listen button and exactly three discussion questions — the console section included. Its questions should be about what the reader found on the benches, not about the prose.

3 · Two themes in one file

The parents disagree about theme: Discussion is warm paper on light, Lab is a dark instrument panel. Do not pick one. Scope the Lab's variables to the console block and let the page stay light around it — the tonal shift is what tells the reader they have moved from reading to operating.

:root{ /* the Discussion page */
  --ink:#FBFAF7; --raised:#FFFFFF; --bone:#17191D;
  --ochre:#0B6553;   /* accent used as TEXT — must clear 4.5:1 */
  --orange:#12B48C;  /* accent used as FILL — chart bars, pressed buttons */
}
.console{ /* the Lab console, scoped — nothing leaks out */
  --c-bg:#0A1219; --c-panel:#101E29; --c-line:#23404F;
  --c-ink:#E7F1F5; --c-dim:#9DB4C0;
  --c-qi:#3FD6AE; --c-ion:#6BB6F7;   /* one hue per subject, used everywhere
  --c-amber:#F5C24A; --c-red:#FF7183;    for that subject */
}
Keep the parent variable NAMES

Retaining --ochre and --orange rather than inventing --jade and --jade-bright means every rule copied from bitcoin.html keeps working after a hue change. qi.html shifted the whole page from orange to jade by editing two values. Rename them and you inherit nothing.

Give each subject one hue and never reuse it. On qi.html everything about qi is --c-qi and everything about ions is --c-ion, in the tab bar, the markers, the gauges and the effect-size dots. The reader learns the mapping in about four seconds and it then does the work of a legend on every subsequent bench.

4 · The console: tabs and benches

The Lab's lesson-tab lifecycle, renamed. Tab id X<div class="bay" id="bay-X">, visibility is purely a class, and every animated bench needs a matching stop call.

var BENCHES=[['ladder','⚖️ Claim ladder'],['room','🌬️ Ion room'],
             ['breath','🫁 Resonance breathing'],['trials','📊 Effect sizes'],
             ['needle','🪡 Needle grasp'],['chance','🎲 Chance vs skill']];

window.showBench=function(id){
  BENCHES.forEach(function(b){
    document.getElementById('bay-'+b[0]).classList.toggle('on', b[0]===id);
    var t=document.getElementById('tab-'+b[0]);
    t.classList.toggle('on', b[0]===id);
    t.setAttribute('aria-selected', b[0]===id?'true':'false');
  });
  // stop on the way OUT, not merely start on the way in —
  // a hidden rAF loop otherwise runs behind the next bench forever
  if(id!=='room')   roomStop();
  if(id!=='breath') breathStop();
  if(id!=='needle') needleStop();
  if(id==='room')   roomKick();
};

Six benches is the working size — enough to cover a real argument, few enough that the tab bar does not wrap twice on a phone. Use the Lab's three figure patterns (scrubbable scalar, live simulation, clickable diagram) and add one shape the Lab does not have:

BenchPatternDoes the argumentative work of…
Claim ladderclickable diagramthe whole page, in one picture — build this first
Live mechanismlive simulationshowing a real effect and its unadvertised cost
Response curvescrubbableletting the reader find an optimum rather than be told it
Effect sizesselector → chart + prosethe new one — "real" versus "big"
Mechanism detailscrubbableone confirmed mechanism, at proper scale
Statistical intuitionlive simulationletting chance imitate a real effect in front of them
The claim ladder is the load-bearing bench

Rungs of evidence from "can it be counted at all" to "is the mechanism confirmed," with every claim placed at the highest rung it actually reached, in its subject's colour. It is the only figure that shows the shape of the disagreement, and it is what the rest of the page keeps pointing back to. Design it before you write the prose; if you cannot place your claims on rungs, the page has no argument yet.

Effect sizes — say how big, not just whether

The bench the Discussion type is missing. Rows carry either a point estimate with a real published confidence interval, or several point estimates, or a plain verdict where no single number exists. Never invent an interval to make the chart look complete.

{id:'acu-sham', side:'qi', scale:'smd', nullv:0,
 pts:[{l:'back & neck pain',v:0.23},{l:'osteoarthritis',v:0.16},{l:'chronic headache',v:0.15}],
 st:'part', ttl:'…', d:'…', s:'Vickers et al., 29 RCTs, 17,922 patients'},

{id:'ion-anx', side:'ion', scale:'note', st:'not', // no pooled number exists
 ttl:'…', d:'…', s:'…'},

scale:'note' renders a word — SUPPORTED / WEAK SIGNAL ONLY / NOT SUPPORTED — instead of a bar. A row with no number is a finding, not a gap, and drawing a bar for it would be a lie about the evidence.

5 · Evidence states, not opinions

The Discussion type labels claims verifiable / contested / argument — that stays, in the prose. The console needs a second, different axis, because "how much evidence is there" is not the same question as "who is asserting this".

Console stateColourMeans
est--c-qi / --c-ion, filledestablished — replicated, or mechanically unambiguous
part--c-amber, hollowone signal, with limits the source itself states
not--c-red, hollowtested and not supported, or never demonstrated

Filled means established; hollow means it stops here. The reader reads the fill before they read the label, so the fill has to carry the truth on its own.

Say "not established" in the same voice as "established"

The temptation is to soften the failures of the subject you find sympathetic and sharpen the ones you do not. The console makes that visible at a glance because both subjects are on one figure in one notation — which is exactly why the type works, and exactly why it is unforgiving of a thumb on the scale.

6 · SVG traps that cost real debugging

Three bugs from the qi.html build. All three passed code review and were caught only by driving the page in a headless browser. Copy the fixes.

1 · offsetParent does not exist on SVG

The Lab template's self-halt idiom is if(!box.offsetParent){ stop(); return; }, written for a <div> host. Every console stage is an <svg>, where offsetParent is undefined — so the guard is always true and every animation loop kills itself on its first frame. The Animate buttons simply did nothing, silently, with no console error.

function onScreen(id){ var e=document.getElementById(id);
  return !!(e && e.getClientRects().length); }   // works for HTML and SVG
2 · fill="none" is not clickable in its interior

SVG's default pointer-events: visiblePainted hit-tests only where paint actually landed. A hollow marker is therefore clickable on its ~1px ring and nowhere else — and since hollow is how the ladder shows weak or failed evidence, ten of sixteen markers were effectively dead.

.hit .mk{pointer-events:all}
3 · decorative rules swallow clicks

Ladder markers sit centred exactly on their rung lines, so the lines intercepted the clicks meant for the markers.

.cgrid,.crule,.cx{pointer-events:none}

And the layout checks worth automating

7 · Grounding and fact-checking

This type puts numbers on screen with citations attached, so it carries more risk than either parent. Two passes, and the second is not optional.

  1. Ground it. Build a NotebookLM notebook and run a deep research sweep on the subject (source add-research "…" --mode deep). Count the sources afterwards — the importer has historically double-imported.
  2. Then verify independently. Send the specific numbers — author, journal, year, effect size, sample size — to a separate web-search pass. On the qi.html build this caught two claims already written into the page: that indoor air is ion-depleted (measurement says the opposite, and it is the premise most ionizer marketing rests on), and a pooled depression effect that turned out to be a within-arm before/after change rather than a placebo-controlled difference.
A citation without its caveat is decoration

Where verification changes the story, put the change on the page rather than quietly dropping the claim. "In mice, and the human follow-up was much weaker" is more useful to a student than either the bare result or its omission. The footer should also say plainly which benches are published findings and which are working models scaled to published measurements — a reader who cites your simulation has been misled by you, not by themselves.

8 · The explainer video

Same pipeline as both parents, and the same rule: do not fork the builder. Config per page, one generic script.

python python/video_gen/build_page_explainer.py qi
  reads  python/video_gen/page_explainers/qi.py
  writes qi_explainer.mp4 + .en.vtt + .zh.vtt
VOICE = "en-US-AriaNeural"      # edge-tts, free
RATE  = "-6%"
LINES = [
  ("English narration line.", "中文字幕。", ("card", "EYEBROW", "Head\nline")),
  ("A drawn one.",            "画的。",     ("figure", "ladder", "caption")),
]
FIGURES = {"ladder": draw_ladder, …}   # PIL functions in the same config
Visual kindUse for
("card", eyebrow, headline)A typographic slide. Numbers, contrasts, statements.
("photo", filename, caption)A real photograph the page already credits.
("spark", prompt, caption[, avoid])An SDXL still from the Spark. Optional 4th element appends to the negative prompt.
("figure", key, caption)A drawn schematic — dispatches to FIGURES[key].
A Hybrid's video should be card + figure, not spark

The console's benches are already specified geometry. Redraw them with PIL in the console's own palette and the viewer who arrives at the page recognises what they are looking at — the video becomes a trailer for the instruments rather than unrelated decoration. Draw the figures at 1080 × 530, the box draw_photo() scales into, so nothing is resampled twice and the mono type stays crisp.

It also means no Spark dependency: qi_explainer.mp4 builds with the box switched off. And a page arguing that people accept invisible claims too readily should not illustrate itself with generated pictures — qi.py declares that in its module docstring, which is where the next person will look.

Embed it under the intro segment's heading, with both subtitle tracks. The 中文 toggle switches the zh track on via syncSubs():

<figure class="vidfig">
  <video controls preload="metadata" playsinline width="1280" height="720">
    <source src="../video/qi_explainer.mp4" type="video/mp4">
    <track kind="subtitles" src="../video/qi_explainer.en.vtt" srclang="en" label="English">
    <track kind="subtitles" src="../video/qi_explainer.zh.vtt" srclang="zh" label="中文">
  </video>
</figure>
Three things that go wrong

Encode for mobile: -profile:v main -level 4.0 -movflags +faststart -pix_fmt yuv420p. Default ffmpeg output plays on a PC and is silently refused by Android and by the LINE in-app player.

Invalidate CloudFront after re-uploading, or the old cut keeps serving: aws cloudfront create-invalidation --distribution-id E2F1VRCSRT1FEV --paths "/video/<name>.mp4" --profile claude-deploy.

Rebuild when the page changes. The video narrates the page's claims in the page's order; restructure the page and it goes stale silently. State the real runtime in the caption rather than guessing it.

Close with a goodbye

Every krueng.ai video ends with a warm sign-off, placed after the closing hook — bitcoin.py, neuro_lab.py and qi.py all do. The packaging linter warns that an exit signal costs retention; that warning is non-blocking and we accept it.

9 · What you inherit unchanged

Copy these from bitcoin.html verbatim. They are documented in the Discussion template and none of them need Hybrid-specific changes:

10 · Testing

Headless Playwright. The Discussion checks all still apply; these are the additions.

CheckAsserts
Benches rendereach bay visible when its tab is active; its stage has > N nodes
Controls do somethinggauges change after driving each control — a correct no-op is indistinguishable from a dead button
Animations stoprAF tick count is 0 shortly after leaving an animated bench
Markers clickableevery marker, by mouse and by Enter — not a sample
viewBox overflowper bench, while visible, at several slider positions
Text collisionspairwise bbox overlap between texts in the same SVG
Contrastlight page and dark console, in every language combination
Layoutno horizontal scroll at 1280 / 900 / 390
Screenshot every bench and look at it

The qi.html suite passed clean while one caption sat outside its viewBox and two others overlapped a data trace. Tests confirm what you thought to ask. Only looking catches the rest.

11 · Build checklist

  1. Pick two claims that are contested, instrumentable, and fail in different places.
  2. Ground it in NotebookLM; then verify the specific numbers independently. Fix what comes back wrong, on the page.
  3. Design the claim ladder first. If the claims will not sit on rungs, stop and rethink the subject.
  4. Copy HTML/qi.html. Rename the localStorage keys.
  5. Write the segments — prose, claim boxes, three questions each. Console section included.
  6. Build the six benches. Wire every animated one into showBench()'s stop list.
  7. Audit the prose for undefined terms; build the vocabulary from what you find.
  8. Write the EN summary per segment, then zh and th.
  9. Write page_explainers/<name>.py — card + figure, figures at 1080 × 530, close with a goodbye. Build the video, state the true runtime, upload mp4 + both VTTs to S3.
  10. Run the suite. Then screenshot every bench and actually look at it.
  11. Add a card to HTML/index.html. Commit on a feature branch and push — never to main, never HTML to S3.
The one rule

Both parents have one. This type's is: the instruments must be able to embarrass you. If every bench flatters the conclusion you had before you built them, you have made an illustration, not an instrument, and the reader will be able to tell.