Los casos diagnosticaban y no exigían. Cada uno cerraba con un veredicto —«un contrato que no
se aplica no es un contrato: es un comentario»— y esa lección no llegaba a ninguna parte: los
nueve apuntaban `related_modes` al modo que ESCRIBE expedientes. Un bucle circular. La serie
denunciaba el contrato desconectado y ella misma lo era.
El esquema ahora lo impide:
lesson_codified_in pares what/where con gramática de punteros verificados
(adr:NNN/id · qa:id · mode:id/paso · gate:just X · test:ruta · contract:ruta#campo)
lesson_debt lo que nada exige todavía, con la dirección de quien lo saldaría
LessonAccounted contrato que RECHAZA el caso que no llena ninguno de los dos.
Visto rechazar antes de rellenar nada. Confesar cuesta (adr-071).
replay la sesión repetida con protocolo: el prompt literal recibido (con su
procedencia declarada — una reconstrucción sin fuente es ficción), el
prompt corregido, las microtareas con su oráculo NOMBRADO, la puerta
antes de delegar y el disparador de adr?.
Contabilizados los diez casos × 2 idiomas. Cuatro llevan replay. El reparto es el hallazgo:
las lecciones que dolieron en MECANISMO acabaron en constraints de ADR; las que dolieron en
CONDUCTA DEL OPERADOR quedaron en deuda. Esa mitad era justo la que faltaba.
Nuevo: /docs/expedientes/protocolo.html y /docs/case-files/protocol.html — el protocolo de
sesión, PROYECTADO de los replays (gen-protocolo.nu, con --check en la cadena). No es una
tarjeta del grid: el grid indexa CASOS, y una proyección archivada entre las instancias de las
que se proyecta es el mismo error de categoría que un esquema archivado entre ellas. Lleva las
cuatro caras de una sola regla —el agente nunca es su propio oráculo— y la deuda viva de la
serie, que se recalcula sola: cuando una ⊘ se salda en su caso, desaparece de la página.
Nuevo expediente 8/0 — la cura que no volvió a casa (ES+EN, 'clinical). Las ocho líneas que
cerraron el caso fundacional existen, probadas y sirviendo, y no habían llegado a ninguna
parte: ni al commit de su instancia (donde alguien siguió añadiendo kinds A MANO hasta siete),
ni a las dos plantillas de las que nace cada sitio. 23 constraints en 8 ADRs, 0 ejecutadas.
Y su punto de abandono: el arquitecto recomendó borrar el atajo con «prueba empírica» construida
sobre dos árboles distintos — habría roto 6 de 8 rutas respondiendo 200. Lo paró el testigo que
el propio encargo exigía. La puerta cazó al arquitecto.
Deuda corregida en 404-PAP y 404-PAP-bis: decían que faltaban tres cosas cuando faltaba una.
El backport está hecho y la constraint escrita; sólo falta que algo la ejecute. Un howto no
expira solo — éste caducó en dos horas.
Y el mensaje de la puerta de ilustración daba rutas que no resuelven desde donde la puerta
corre. Corregido: las rutas son de la raíz de la constelación, y el conversor NO reescala.
Verificado: 22 casos reproducen · completos en en, es · servidos · informe reproduce ·
parity 11×2 · protocolo-check reproduce · oráculo de cobertura ALL PASS.
399 lines
25 KiB
HTML
399 lines
25 KiB
HTML
<!doctype html>
|
||
<html lang="{{LANG}}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>{{TITLE}}</title>
|
||
<style>
|
||
/* ── PALETAS por skin × tema ─────────────────────────────────────────── */
|
||
:root, :root[data-skin="detective"]{
|
||
--paper:#F6F1E4; --paper-edge:#E4DAC2; --ink:#1C1810; --ink-faded:#5A5240;
|
||
--desk:#33302A; --stamp:#9C2D25; --accent:#8A6014; --rule:#CDBF9C; --code:#26231D;
|
||
}
|
||
:root[data-skin="detective"][data-theme="dark"]{
|
||
--paper:#D9CBA6; --paper-edge:#C7B78E; --ink:#231D14; --ink-faded:#574F3B;
|
||
--desk:#100E0A; --stamp:#CF4133; --accent:#C89338; --rule:#B6A67E; --code:#0A0805;
|
||
}
|
||
:root[data-skin="clinical"]{
|
||
--paper:#F5F9FA; --paper-edge:#DBE7EA; --ink:#132630; --ink-faded:#51666E;
|
||
--desk:#17282E; --stamp:#C0272B; --accent:#0C7A83; --rule:#D2E0E4; --code:#0C1A20;
|
||
}
|
||
:root[data-skin="clinical"][data-theme="dark"]{
|
||
--paper:#D7E4E7; --paper-edge:#BFD2D6; --ink:#0F2028; --ink-faded:#4A5F67;
|
||
--desk:#081217; --stamp:#E24A44; --accent:#2FA7B0; --rule:#A9C2C8; --code:#050D11;
|
||
}
|
||
|
||
*{box-sizing:border-box}
|
||
html{-webkit-text-size-adjust:100%}
|
||
body{margin:0; min-height:100vh;
|
||
background:radial-gradient(120% 80% at 82% -12%, color-mix(in srgb,var(--accent) 20%,transparent), transparent 60%), var(--desk);
|
||
color:var(--ink); font-family:Georgia,"Iowan Old Style","Times New Roman",serif; line-height:1.6;
|
||
padding:0 1rem 4rem; display:flex; flex-direction:column; align-items:center;
|
||
--mono:"Courier New",ui-monospace,"Cascadia Mono",monospace}
|
||
|
||
/* ── Barra de navegación del sitio (logo + volver) ───────────────────── */
|
||
.sitebar{width:100%; max-width:52rem; display:flex; align-items:center; justify-content:space-between;
|
||
gap:1rem; padding:.9rem .2rem; margin-bottom:.4rem}
|
||
.sitebar a{color:var(--paper); text-decoration:none; font-family:var(--mono); font-size:.76rem; letter-spacing:.04em}
|
||
.sitebar .brand{display:flex; align-items:center; gap:.55rem; font-weight:700}
|
||
.sitebar .brand img{height:3rem; width:auto; display:block}
|
||
.sitebar .back{border:1px solid color-mix(in srgb,var(--paper) 45%,transparent); padding:.34rem .7rem; border-radius:.4rem}
|
||
.sitebar .back:hover{border-color:var(--paper); background:color-mix(in srgb,var(--paper) 12%,transparent)}
|
||
|
||
/* ── Conmutador de expedientes ───────────────────────────────────────── */
|
||
.switch{width:100%; max-width:52rem; display:flex; align-items:center; gap:.6rem; flex-wrap:wrap;
|
||
padding:.55rem .8rem; margin-bottom:.9rem; border-radius:.5rem; font-family:var(--mono);
|
||
background:color-mix(in srgb,var(--paper) 12%,transparent); color:var(--paper)}
|
||
.switch .idx{font-size:.72rem; letter-spacing:.1em; opacity:.85; white-space:nowrap}
|
||
.switch .tabs{display:flex; gap:.4rem; flex:1; flex-wrap:wrap}
|
||
.switch .tab{font-size:.72rem; letter-spacing:.03em; background:transparent; color:var(--paper);
|
||
border:1px solid color-mix(in srgb,var(--paper) 40%,transparent); padding:.3rem .6rem; border-radius:.35rem; cursor:pointer}
|
||
.switch .tab[aria-current="true"]{background:var(--paper); color:var(--desk); font-weight:700; border-color:var(--paper)}
|
||
.switch .nav{display:flex; gap:.35rem}
|
||
.switch .nav button{font-size:.72rem; background:transparent; color:var(--paper);
|
||
border:1px solid color-mix(in srgb,var(--paper) 40%,transparent); padding:.3rem .55rem; border-radius:.35rem; cursor:pointer}
|
||
.switch .nav button:disabled{opacity:.35; cursor:default}
|
||
|
||
.file{width:100%; max-width:52rem; background:var(--paper); border:1px solid var(--paper-edge);
|
||
box-shadow:0 1px 0 color-mix(in srgb,#fff 30%,transparent) inset, 0 24px 60px -20px rgba(0,0,0,.6);
|
||
padding:clamp(1.25rem,4.5vw,3rem); position:relative; overflow:hidden;
|
||
background-image:repeating-linear-gradient(0deg, transparent 0 27px, color-mix(in srgb,var(--rule) 20%,transparent) 27px 28px);
|
||
background-attachment:local}
|
||
.file::before{content:""; position:absolute; left:15px; top:0; bottom:0; width:14px;
|
||
background:radial-gradient(circle at 50% 60px,var(--desk) 5px,transparent 6px),
|
||
radial-gradient(circle at 50% 160px,var(--desk) 5px,transparent 6px),
|
||
radial-gradient(circle at 50% 260px,var(--desk) 5px,transparent 6px);
|
||
background-repeat:no-repeat; opacity:.45}
|
||
|
||
.mono{font-family:var(--mono)} a{color:var(--stamp)}
|
||
header{border-bottom:2px solid var(--ink); padding-bottom:1rem; margin-bottom:1.7rem}
|
||
.kicker{font-family:var(--mono); font-weight:700; font-size:.74rem; letter-spacing:.18em; text-transform:uppercase; color:var(--ink-faded)}
|
||
/* The stamp is position:absolute in the header's top-right corner and the title reserved NO room
|
||
for it, so a long title ran straight underneath. The Spanish titles happened to be short enough
|
||
to miss it; the English ones are not — «The case of the validator that never ran» collided with
|
||
DISCHARGED. Nothing was wrong with the stamp: the h1 simply had no boundary. So it gets one, and
|
||
it wraps before the corner instead of under it. Released on narrow screens, where the media
|
||
query below moves the stamp to the bottom and the gutter would be dead space. */
|
||
h1{font-family:var(--mono); font-weight:700; font-size:clamp(1.55rem,5.4vw,2.6rem); line-height:1.05;
|
||
margin:.4rem 0 .2rem; text-wrap:balance; letter-spacing:-.01em; max-width:calc(100% - 13.5rem)}
|
||
@media (max-width:34rem){ h1{max-width:none} }
|
||
h1 .red{color:var(--stamp)}
|
||
.logline{font-style:italic; max-width:44ch; margin:.6rem 0 0}
|
||
.meta{display:flex; flex-wrap:wrap; gap:.4rem .5rem; margin-top:1.1rem; font-family:var(--mono); font-size:.68rem}
|
||
.tag{border:1px solid var(--ink); padding:.22rem .5rem; letter-spacing:.1em; text-transform:uppercase}
|
||
.tag b{color:var(--stamp)} .tag.closed{background:var(--ink); color:var(--paper)}
|
||
|
||
.glossary{border:1px dashed var(--ink); padding:.8rem 1rem; margin:1.5rem 0 0; background:color-mix(in srgb,var(--accent) 8%,var(--paper))}
|
||
.gl-title{font-family:var(--mono); font-weight:700; font-size:.66rem; letter-spacing:.14em; text-transform:uppercase; color:var(--ink-faded); margin-bottom:.5rem}
|
||
.glossary dl{margin:0; display:grid; grid-template-columns:max-content 1fr; gap:.35rem .9rem}
|
||
.glossary dt{font-family:var(--mono); font-weight:700; color:var(--stamp)}
|
||
.glossary dd{margin:0; font-size:.9rem}
|
||
.glossary .more{font-family:var(--mono); font-size:.72rem; margin-top:.55rem}
|
||
|
||
section{margin:2.1rem 0}
|
||
.exhibit{font-family:var(--mono); font-weight:700; font-size:.74rem; letter-spacing:.14em; text-transform:uppercase;
|
||
color:var(--stamp); display:flex; align-items:center; gap:.6rem; margin-bottom:.7rem}
|
||
.exhibit::after{content:""; flex:1; height:1px; background:var(--rule)}
|
||
h2{font-family:var(--mono); font-size:1.22rem; margin:.1rem 0 .6rem; letter-spacing:-.01em}
|
||
p{margin:.7rem 0}
|
||
|
||
.sheet{overflow-x:auto; border:1px solid var(--ink); background:color-mix(in srgb,var(--paper) 80%,#fff)}
|
||
/* SCOPED TO `.sheet`, WHICH IS WHAT THEY WERE ALWAYS FOR — the cost ledger and the "what it left"
|
||
ledger, the only two tables wrapped in it. Written as BARE element selectors (`table`, `td`,
|
||
`td:last-child`) they reached every table in the document, and the one they should never have
|
||
touched is the containment table in the verdict box:
|
||
· `white-space:nowrap` → the mechanism column could not wrap, so a long cell ran straight
|
||
off the page. Only the three cases with long cells showed it.
|
||
· `font-weight:700` + `text-align:right` → the mechanism read as a bolded number column.
|
||
· `font-family:var(--mono)` → both of its columns went monospace, which is why `.cont
|
||
td:last-child` had to declare mono EXPLICITLY: the base was never
|
||
meant to be mono.
|
||
A rule with no boundary is a rule about everything. */
|
||
.sheet table{width:100%; border-collapse:collapse; font-family:var(--mono); font-size:.82rem; min-width:30rem}
|
||
.sheet caption{font-family:var(--mono); font-weight:700; font-size:.68rem; letter-spacing:.13em; text-transform:uppercase; color:var(--ink-faded);
|
||
text-align:left; padding:.5rem .7rem; border-bottom:1px solid var(--rule)}
|
||
.sheet td{padding:.5rem .7rem; border-bottom:1px dashed var(--rule)}
|
||
.sheet td:last-child{text-align:right; font-variant-numeric:tabular-nums; font-weight:700; white-space:nowrap}
|
||
.sheet tr:last-child td{border-bottom:none}
|
||
.sheet tr.big td{background:color-mix(in srgb,var(--stamp) 10%,transparent)}
|
||
/* The `nowrap` above is for NUMBERS — the cost ledger, where a figure must never break. The
|
||
"what it left" ledger reuses the same sheet and its right column holds PROSE, so the same rule
|
||
ran the sentence off the page. Same defect as the containment table, one table down: a rule
|
||
that fits its first table and is applied to its second. */
|
||
.sheet table.yield td:last-child{white-space:normal; overflow-wrap:anywhere}
|
||
/* An emphasised row is already marked TWICE — the tinted background and the stamp colour. The
|
||
`font-size:1.05rem` on top of that (+28% over the .82rem base) was a third mark, and it was the
|
||
only one that changed the LAYOUT: it broke the ledger's rhythm, and on the prose ledger it made
|
||
the sentence shout over the table it lives in. Emphasis is what the row MEANS, not how big it
|
||
is. Colour and background carry it; the size bump only cost line-up. */
|
||
.sheet tr.big td:last-child{color:var(--stamp)}
|
||
.unit{color:var(--ink-faded); font-weight:400}
|
||
|
||
.lineup{display:grid; grid-template-columns:repeat(auto-fill,minmax(13.5rem,1fr)); gap:.8rem; margin-top:1rem}
|
||
.card{border:1px solid var(--ink); background:color-mix(in srgb,var(--paper) 86%,#fff); padding:.7rem .8rem}
|
||
.card .no{font-family:var(--mono); font-size:.66rem; color:var(--ink-faded); letter-spacing:.14em; font-weight:700}
|
||
.card h3{font-family:var(--mono); font-size:.9rem; margin:.15rem 0 .35rem}
|
||
.card p{font-size:.86rem; margin:.2rem 0; font-style:italic}
|
||
.vtag{font-family:var(--mono); font-size:.62rem; letter-spacing:.12em; text-transform:uppercase; display:inline-block;
|
||
margin-top:.45rem; padding:.12rem .4rem; border:1px solid var(--stamp); color:var(--stamp)}
|
||
|
||
pre{font-family:var(--mono); font-size:.78rem; line-height:1.5; overflow-x:auto; background:var(--code); color:#E9DFC6;
|
||
border:1px solid #000; padding:.9rem 1rem; margin:.8rem 0; box-shadow:0 8px 20px -12px rgba(0,0,0,.7)}
|
||
pre .c{color:#8a8570} pre .k{color:var(--accent)} pre .r{color:var(--stamp); font-weight:700} pre .g{color:#89a05a}
|
||
.caplabel{font-family:var(--mono); font-weight:700; font-size:.66rem; letter-spacing:.12em; text-transform:uppercase; color:var(--ink-faded)}
|
||
|
||
.stamp{position:absolute; top:6.6rem; right:1.4rem; transform:rotate(-9deg); border:.32rem solid var(--stamp); color:var(--stamp);
|
||
font-family:var(--mono); font-weight:700; padding:.42rem 1rem; text-align:center; line-height:1.05; opacity:.96; border-radius:7px;
|
||
background:color-mix(in srgb,var(--paper) 78%,transparent); box-shadow:0 0 0 2px color-mix(in srgb,var(--stamp) 26%,transparent);
|
||
pointer-events:none; letter-spacing:.06em}
|
||
.stamp b{font-size:1.2rem; display:block; letter-spacing:.14em} .stamp span{font-size:.64rem; letter-spacing:.2em}
|
||
@media (max-width:34rem){.stamp{animation:none; top:auto; bottom:1rem; right:.8rem; transform:rotate(-8deg); padding:.32rem .7rem} .stamp b{font-size:1rem}}
|
||
|
||
.verdict{border:2px solid var(--ink); padding:1.1rem 1.2rem; background:color-mix(in srgb,var(--accent) 9%,var(--paper))}
|
||
.verdict h2{color:var(--stamp)}
|
||
/* `table-layout:fixed` is load-bearing, not cosmetic. Without it the browser sizes the columns
|
||
to their CONTENT and ignores the 52% — so a long mechanism in the monospace column blows the
|
||
table past the bordered verdict box and the text runs off the page. It only showed on the
|
||
three cases whose cells are long (213–308 chars); the short ones (60–87) fitted by luck.
|
||
The declared width is only a declaration if something makes it bind. */
|
||
.cont{width:100%; border-collapse:collapse; margin-top:.6rem; font-size:.92rem; table-layout:fixed}
|
||
.cont td{padding:.5rem .6rem; border-bottom:1px solid var(--rule); vertical-align:top; overflow-wrap:anywhere}
|
||
.cont td:first-child{width:52%; font-style:italic}
|
||
.cont td:last-child{font-family:var(--mono); font-size:.8rem}
|
||
.kicknum{font-family:var(--mono); color:var(--stamp); font-weight:700}
|
||
|
||
.filefoot{display:flex; align-items:center; justify-content:space-between; gap:1rem; flex-wrap:wrap; margin-top:1.4rem}
|
||
.printlink{font-family:var(--mono); font-size:.72rem; color:var(--stamp); text-decoration:none;
|
||
border:1px solid var(--stamp); padding:.34rem .7rem; border-radius:.4rem; background:transparent; cursor:pointer}
|
||
.printlink:hover{background:color-mix(in srgb,var(--stamp) 12%,transparent)}
|
||
.pager{display:flex; gap:.4rem; font-family:var(--mono)}
|
||
.pager button{font-size:.72rem; background:var(--paper); color:var(--ink); border:1px solid var(--ink);
|
||
padding:.34rem .6rem; border-radius:.4rem; cursor:pointer}
|
||
.pager button:disabled{opacity:.35; cursor:default}
|
||
|
||
footer{margin-top:2.1rem; border-top:1px solid var(--rule); padding-top:.9rem; font-family:var(--mono); font-size:.68rem;
|
||
color:var(--ink-faded); display:flex; justify-content:space-between; flex-wrap:wrap; gap:.5rem}
|
||
|
||
.controls{position:fixed; top:.7rem; right:.7rem; display:flex; gap:.35rem; z-index:20; font-family:var(--mono)}
|
||
.controls button{font-size:.66rem; letter-spacing:.06em; background:var(--paper); color:var(--ink); border:1px solid var(--ink);
|
||
padding:.35rem .55rem; cursor:pointer; border-radius:4px}
|
||
.controls button[aria-pressed="true"]{background:var(--ink); color:var(--paper)}
|
||
.controls button:focus-visible, a:focus-visible, .tab:focus-visible, .pager button:focus-visible{outline:2px solid var(--stamp); outline-offset:2px}
|
||
|
||
@keyframes thunk{0%{transform:rotate(-9deg) scale(2.4); opacity:0} 70%{transform:rotate(-9deg) scale(.94); opacity:1} 100%{transform:rotate(-9deg) scale(1); opacity:.96}}
|
||
.stamp{animation:thunk .5s cubic-bezier(.2,1.3,.4,1) .35s both}
|
||
@media (prefers-reduced-motion:reduce){.stamp{animation:none}}
|
||
|
||
/* Archival/print: hide site chrome, keep the file. */
|
||
@media print{
|
||
.sitebar,.switch,.controls,.filefoot{display:none!important}
|
||
body{background:#fff;padding:0;display:block}
|
||
.file{box-shadow:none;border:none;max-width:none}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="controls">
|
||
<button id="sk-det" aria-pressed="true"></button>
|
||
<button id="sk-cli" aria-pressed="false"></button>
|
||
<button id="tg"></button>
|
||
</div>
|
||
|
||
<nav class="sitebar" aria-label="ontoref">
|
||
<!-- The logo IS the wordmark (the SVG carries the letters), so the <span> beside it said the name
|
||
a second time: at 50px it reads "Ontoref ontoref" and the two overlap. At 24px the letters
|
||
inside the mark were too small to read, which is why the duplication went unnoticed — the
|
||
defect was always there, it was just below the resolution at which anyone could see it. -->
|
||
<a class="brand" href="/"><img src="/images/ontoref-logo.svg" alt="ontoref"></a>
|
||
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
|
||
<a class="back" href="{{PROTO_HREF}}">{{PROTO_LABEL}}</a>
|
||
<a class="back" id="sw-back" href="#"></a>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="switch" role="tablist" id="sw-bar">
|
||
<span class="idx" id="sw-idx"></span>
|
||
<div class="tabs" id="sw-tabs"></div>
|
||
<div class="nav">
|
||
<button id="sw-prev">‹</button>
|
||
<button id="sw-next">›</button>
|
||
</div>
|
||
</div>
|
||
|
||
<article class="file" id="file"></article>
|
||
|
||
<script>
|
||
(function(){
|
||
"use strict";
|
||
|
||
/* ══ DATA ══ Projected by scripts/build/gen-informe.nu from the typed CASEs under
|
||
site/content/expedientes. DO NOT EDIT THIS FILE — edit the .ncl.
|
||
This viewer was hand-kept for two cases and drifted from the articles it shares a
|
||
source with (deriva's ledger: 6 rows there, 8 here). */
|
||
|
||
{{DATA}}
|
||
|
||
/* ══ RENDER ══ */
|
||
var skinKey = "detective";
|
||
var idx = 0;
|
||
|
||
function render(){
|
||
var c = CASES[idx], s = c.skins[skinKey] || c.skins[c.skin];
|
||
document.documentElement.setAttribute("data-skin", skinKey);
|
||
document.title = UI.doc_title.replace("{case_no}", c.caseNo);
|
||
// A case may legitimately have one skin. Better a dead button, plainly dead, than
|
||
// prose invented to fill the other genre.
|
||
var det = document.getElementById("sk-det"), cli = document.getElementById("sk-cli");
|
||
det.setAttribute("aria-pressed", skinKey==="detective");
|
||
cli.setAttribute("aria-pressed", skinKey==="clinical");
|
||
det.disabled = !c.skins.detective; det.title = c.skins.detective ? "" : UI.skin_missing;
|
||
cli.disabled = !c.skins.clinical; cli.title = c.skins.clinical ? "" : UI.skin_missing;
|
||
|
||
// Switcher chrome
|
||
document.getElementById("sw-idx").textContent = UI.counter + " " + (idx+1) + "/" + CASES.length;
|
||
document.getElementById("sw-prev").disabled = (idx===0);
|
||
document.getElementById("sw-next").disabled = (idx===CASES.length-1);
|
||
document.getElementById("sw-tabs").innerHTML = CASES.map(function(x,i){
|
||
return "<button class='tab' role='tab' data-go='"+i+"' aria-current='"+(i===idx)+"'>"+x.tab+"</button>";
|
||
}).join("");
|
||
|
||
var cost = c.cost.map(function(r){
|
||
return "<tr"+(r[2]?" class='big'":"")+"><td>"+r[0]+"</td><td>"+r[1]
|
||
.replace("<i>","<span class='unit'>").replace("</i>","</span>")+"</td></tr>";
|
||
}).join("");
|
||
var susp = c.suspectsTech.map(function(h,i){
|
||
var n=(i+1<10?"0":"")+(i+1);
|
||
return "<div class='card'><div class='no'>"+s.no.toUpperCase()+" "+n+"</div><h3>"+h+
|
||
"</h3><p>"+s.alibis[i]+"</p><span class='vtag'>"+s.tags[i]+"</span></div>";
|
||
}).join("");
|
||
var gloss = c.glossary.map(function(g){return "<dt>"+g.t+"</dt><dd>"+g.d+"</dd>";}).join("");
|
||
var cont = c.contain.map(function(r){return "<tr><td>"+r[0]+"</td><td>"+r[1]+"</td></tr>";}).join("");
|
||
var yieldTbl = "";
|
||
if(c.yielded && c.yielded.length){
|
||
var yrows = c.yielded.map(function(r){
|
||
return "<tr"+(r[2]?" class='big'":"")+"><td>"+r[0]+"</td><td>"+r[1]
|
||
.replace("<i>","<span class='unit'>").replace("</i>","</span>")+"</td></tr>";
|
||
}).join("");
|
||
yieldTbl = "<div class='sheet'><table class='yield'><caption>"+c.yieldedTitle+"</caption><tbody>"+yrows+"</tbody></table></div>";
|
||
}
|
||
|
||
// The replay and the lesson's ledger — same data the article renders; a case without a
|
||
// replay simply has no such section (an admitted gap, never invented prose).
|
||
var replayHtml = "";
|
||
if(c.replay){
|
||
var rsteps = c.replay.steps.map(function(r){return "<tr><td>"+r[0]+"</td><td>"+r[1]+"</td></tr>";}).join("");
|
||
replayHtml =
|
||
"<section><h2>"+s.replayH+"</h2>"+
|
||
"<span class='caplabel'>"+L.replay_before+" — "+L.replay_source+" <span class='mono'>"+c.replay.beforeSrc+"</span></span>"+
|
||
"<pre>"+c.replay.before+"</pre>"+
|
||
"<span class='caplabel'>"+L.replay_after+"</span>"+
|
||
"<pre>"+c.replay.after+"</pre>"+
|
||
"<table class='cont'><tbody><tr><td><b>"+L.replay_task+"</b></td><td><b>"+L.replay_verif+"</b></td></tr>"+rsteps+"</tbody></table>"+
|
||
"<p><b>"+L.replay_gate+":</b> "+c.replay.gate+"</p>"+
|
||
(c.replay.adr ? "<p><b>"+L.replay_adr+":</b> "+c.replay.adr+"</p>" : "")+
|
||
"</section>";
|
||
}
|
||
var lessonHtml = "";
|
||
if((c.lesson.edges && c.lesson.edges.length) || c.lesson.debt){
|
||
var lrows = c.lesson.edges.map(function(r){return "<tr><td>"+r[0]+"</td><td><span class='mono'>"+r[1]+"</span></td></tr>";}).join("");
|
||
lessonHtml =
|
||
"<section><h2>"+s.lessonH+"</h2>"+
|
||
(lrows ? "<table class='cont'><tbody>"+lrows+"</tbody></table>" : "")+
|
||
(c.lesson.debt ? "<p>⊘ <b>"+L.debt_label+":</b> "+c.lesson.debt+"</p>" : "")+
|
||
"</section>";
|
||
}
|
||
|
||
document.getElementById("file").innerHTML =
|
||
"<div class='stamp' aria-hidden='true'><b>"+s.stamp[0]+"</b><span>"+s.stamp[1]+"</span></div>"+
|
||
"<header><div class='kicker'>"+s.kicker+"</div>"+
|
||
"<h1>"+s.h1[0]+"<span class='red'>"+s.h1[1]+"</span></h1>"+
|
||
"<p class='logline'>"+s.logline+"</p>"+
|
||
"<div class='meta'>"+
|
||
"<span class='tag'>"+s.meta[0]+" <b>"+c.caseNo+"</b></span>"+
|
||
"<span class='tag'>"+s.meta[1]+": <b>"+c.classification+"</b></span>"+
|
||
"<span class='tag'>"+s.meta[2]+"</span>"+
|
||
"<span class='tag closed'>"+s.meta[3]+"</span></div>"+
|
||
"<aside class='glossary'><div class='gl-title'>"+UI.gloss_title+"</div>"+
|
||
"<dl>"+gloss+"</dl><div class='more'>"+c.glossaryMore+"</div></aside>"+
|
||
"</header>"+
|
||
"<section><div class='exhibit'>"+s.ex[0]+"</div><h2>"+s.h2[0]+"</h2><p>"+s.lead2+"</p>"+
|
||
"<div class='sheet'><table><caption>"+c.costCaption+"</caption><tbody>"+cost+"</tbody></table></div>"+yieldTbl+"</section>"+
|
||
"<section><div class='exhibit'>"+s.ex[1]+"</div><h2>"+s.h2[1]+"</h2><p>"+s.lead3+"</p>"+
|
||
"<div class='lineup'>"+susp+"</div></section>"+
|
||
"<section><div class='exhibit'>"+s.ex[2]+"</div><h2>"+s.h2[2]+"</h2>"+
|
||
"<span class='caplabel'>"+s.weaponCap+"</span><pre>"+c.weaponCode+"</pre><p>"+s.weaponAfter+"</p></section>"+
|
||
"<section><div class='exhibit'>"+s.ex[3]+"</div><h2>"+s.h2[3]+"</h2>"+
|
||
"<span class='caplabel'>"+s.fixCap+"</span><pre>"+c.fixCode+"</pre><p>"+s.fixAfter+"</p></section>"+
|
||
"<section class='verdict'><div class='exhibit'>"+s.ex[4]+"</div><h2>"+s.h2[4]+"</h2>"+
|
||
"<p>"+s.verdictLead+"</p><table class='cont'><tbody>"+cont+"</tbody></table>"+
|
||
"<p style='margin-bottom:0'>"+c.verdictClose+"</p></section>"+
|
||
replayHtml+
|
||
lessonHtml+
|
||
"<div class='filefoot'>"+
|
||
"<button type='button' class='printlink' data-print>"+UI.print+"</button>"+
|
||
"<div class='pager'>"+
|
||
"<button data-step='-1'"+(idx===0?" disabled":"")+">"+UI.pager_prev+"</button>"+
|
||
"<button data-step='1'"+(idx===CASES.length-1?" disabled":"")+">"+UI.pager_next+"</button>"+
|
||
"</div></div>"+
|
||
"<footer><span>"+s.footL+"</span><span>"+s.footR+"</span></footer>";
|
||
}
|
||
|
||
function goto(i){
|
||
if(i<0||i>=CASES.length) return;
|
||
idx=i;
|
||
var id=CASES[idx].id;
|
||
// The hash is the SLUG (localised, public) — not the id (the Spanish join key).
|
||
var pub = (CASES[idx] && CASES[idx].slug) ? CASES[idx].slug : id;
|
||
if(location.hash.slice(1)!==pub) history.replaceState(null,"","#"+pub);
|
||
render();
|
||
}
|
||
|
||
var root=document.documentElement;
|
||
root.setAttribute("data-theme", matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light");
|
||
document.getElementById("sk-det").textContent = UI.skin_detective;
|
||
document.getElementById("sk-cli").textContent = UI.skin_clinical;
|
||
document.getElementById("tg").setAttribute("aria-label", UI.theme);
|
||
document.getElementById("tg").textContent = "☀ / ☾";
|
||
var back = document.getElementById("sw-back");
|
||
back.textContent = UI.back; back.setAttribute("href", UI.back_href);
|
||
document.getElementById("sw-bar").setAttribute("aria-label", UI.switch_label);
|
||
document.getElementById("sw-prev").setAttribute("aria-label", UI.prev_label);
|
||
document.getElementById("sw-next").setAttribute("aria-label", UI.next_label);
|
||
document.getElementById("sk-det").onclick=function(){skinKey="detective";render();};
|
||
document.getElementById("sk-cli").onclick=function(){skinKey="clinical";render();};
|
||
document.getElementById("tg").onclick=function(){
|
||
root.setAttribute("data-theme", root.getAttribute("data-theme")==="dark"?"light":"dark");
|
||
};
|
||
document.getElementById("sw-prev").onclick=function(){goto(idx-1);};
|
||
document.getElementById("sw-next").onclick=function(){goto(idx+1);};
|
||
// Delegated: tab clicks + pager buttons inside #file.
|
||
document.addEventListener("click",function(ev){
|
||
var t=ev.target.closest && ev.target.closest("[data-go],[data-step],[data-print]");
|
||
if(!t) return;
|
||
if(t.hasAttribute("data-print")){
|
||
var href = CASES[idx].printHref;
|
||
if(href) window.open(href, "_blank", "noopener"); else window.print();
|
||
return;
|
||
}
|
||
if(t.hasAttribute("data-go")) goto(parseInt(t.getAttribute("data-go"),10));
|
||
else goto(idx+parseInt(t.getAttribute("data-step"),10));
|
||
});
|
||
// Accept EITHER the slug (what we now emit) or the id (what was emitted before, and what any
|
||
// already-shared link carries). A rename that breaks the links people saved is not a fix.
|
||
function findCase(h){
|
||
if(!h) return -1;
|
||
var i=CASES.findIndex(function(x){return x.slug===h;});
|
||
return i!==-1 ? i : CASES.findIndex(function(x){return x.id===h;});
|
||
}
|
||
|
||
window.addEventListener("hashchange",function(){
|
||
var i=findCase(location.hash.slice(1));
|
||
if(i>=0 && i!==idx) goto(i);
|
||
});
|
||
|
||
// Open on the deep-linked case if the hash names one.
|
||
var start=findCase(location.hash.slice(1));
|
||
idx = start>=0 ? start : 0;
|
||
render();
|
||
})();
|
||
</script>
|
||
|
||
</body></html>
|