|
| 1 | +<!-- Deterministic: render(t) draws the exact frame at t ms. No animation, no |
| 2 | + clocks, no randomness -- so the renderer can screenshot it frame by frame |
| 3 | + and get the same video every time. --> |
| 4 | +<title>Parallax — backup demo</title> |
| 5 | +<style> |
| 6 | + :root { |
| 7 | + --bg: #0b0f14; --panel: #121821; --line: #1f2937; --ink: #e6edf3; |
| 8 | + --dim: #8b98a9; --blue: #4c8dff; --green: #3fb950; --red: #f85149; |
| 9 | + --wa: #005c4b; --wa-in: #1f2c34; |
| 10 | + --mono: "SF Mono", "Menlo", "DejaVu Sans Mono", monospace; |
| 11 | + --sans: -apple-system, "Segoe UI", Inter, system-ui, sans-serif; |
| 12 | + } |
| 13 | + * { box-sizing: border-box; margin: 0; padding: 0; } |
| 14 | + body { width: 1920px; height: 1080px; background: var(--bg); color: var(--ink); |
| 15 | + font-family: var(--sans); overflow: hidden; } |
| 16 | + #stage { display: grid; grid-template-rows: 104px 1fr 128px; height: 1080px; } |
| 17 | + |
| 18 | + header { display: flex; align-items: center; gap: 20px; padding: 0 56px; |
| 19 | + border-bottom: 1px solid var(--line); } |
| 20 | + .word { font-size: 34px; font-weight: 640; letter-spacing: -0.5px; } |
| 21 | + .tag { font-size: 21px; color: var(--dim); } |
| 22 | + .badge { margin-left: auto; font-family: var(--mono); font-size: 16px; color: var(--dim); |
| 23 | + border: 1px solid var(--line); border-radius: 999px; padding: 8px 18px; } |
| 24 | + |
| 25 | + .cols { display: grid; grid-template-columns: 712px 1fr; gap: 44px; padding: 36px 56px; |
| 26 | + min-height: 0; } |
| 27 | + |
| 28 | + /* phone */ |
| 29 | + .phone { background: #0d1418; border: 1px solid var(--line); border-radius: 30px; |
| 30 | + display: grid; grid-template-rows: 74px 1fr; overflow: hidden; min-height: 0; } |
| 31 | + .phone-bar { display: flex; align-items: center; gap: 14px; padding: 0 24px; |
| 32 | + background: var(--wa-in); border-bottom: 1px solid #000; } |
| 33 | + .avatar { width: 40px; height: 40px; border-radius: 50%; background: var(--blue); |
| 34 | + display: grid; place-items: center; font-weight: 700; font-size: 17px; color: #04101f; } |
| 35 | + .who { font-size: 19px; font-weight: 600; } |
| 36 | + .who small { display: block; font-size: 14px; color: var(--dim); font-weight: 400; } |
| 37 | + .thread { padding: 22px; overflow: hidden; display: flex; flex-direction: column; |
| 38 | + gap: 14px; justify-content: flex-end; min-height: 0; |
| 39 | + -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 58px, #000 100%); } |
| 40 | + .msg { max-width: 90%; padding: 12px 16px; border-radius: 14px; font-size: 15px; |
| 41 | + line-height: 1.45; white-space: pre-wrap; word-break: break-word; |
| 42 | + font-family: var(--mono); } |
| 43 | + .msg.in { align-self: flex-end; background: var(--wa); } |
| 44 | + .msg.out { align-self: flex-start; background: var(--wa-in); } |
| 45 | + .msg b { color: #7ee0c0; font-weight: 600; } |
| 46 | + .typing { align-self: flex-start; background: var(--wa-in); padding: 15px 20px; |
| 47 | + border-radius: 14px; display: flex; gap: 7px; } |
| 48 | + .typing i { width: 9px; height: 9px; border-radius: 50%; background: var(--dim); display: block; } |
| 49 | + |
| 50 | + /* proof */ |
| 51 | + .proof { background: var(--panel); border: 1px solid var(--line); border-radius: 18px; |
| 52 | + padding: 30px 34px; font-family: var(--mono); font-size: 19px; line-height: 1.75; |
| 53 | + overflow: hidden; min-height: 0; } |
| 54 | + .proof .k { color: var(--dim); } |
| 55 | + .proof .ok { color: var(--green); } |
| 56 | + .proof .no { color: var(--red); } |
| 57 | + .proof .hi { color: var(--ink); font-weight: 600; } |
| 58 | + .proof .b { color: var(--blue); } |
| 59 | + .proof h3 { font-family: var(--sans); font-size: 15px; letter-spacing: 1.6px; |
| 60 | + text-transform: uppercase; color: var(--dim); font-weight: 600; |
| 61 | + margin: 26px 0 12px; } |
| 62 | + .proof h3:first-child { margin-top: 0; } |
| 63 | + .note { color: var(--dim); font-size: 16px; line-height: 1.6; font-family: var(--sans); } |
| 64 | + .steps { list-style: none; font-family: var(--sans); font-size: 18px; } |
| 65 | + .steps li { display: flex; align-items: center; gap: 14px; padding: 7px 0; color: var(--dim); } |
| 66 | + .steps li .dot { width: 11px; height: 11px; border-radius: 50%; background: #2b3648; |
| 67 | + flex: none; } |
| 68 | + .steps li.on { color: var(--ink); } |
| 69 | + .steps li.on .dot { background: var(--blue); box-shadow: 0 0 0 5px rgba(76,141,255,.16); } |
| 70 | + .steps li.done { color: var(--dim); } |
| 71 | + .steps li.done .dot { background: var(--green); box-shadow: none; } |
| 72 | + |
| 73 | + .caption { border-top: 1px solid var(--line); padding: 0 56px; display: flex; |
| 74 | + align-items: center; } |
| 75 | + .caption p { font-size: 27px; line-height: 1.35; max-width: 1600px; } |
| 76 | + .caption .em { color: var(--blue); } |
| 77 | + .hidden { display: none; } |
| 78 | +</style> |
| 79 | + |
| 80 | +<div id="stage"> |
| 81 | + <header> |
| 82 | + <span class="word">Parallax</span> |
| 83 | + <span class="tag">Simula el cambio antes de aplicarlo. Y te decimos cuánto de eso fue real.</span> |
| 84 | + <span class="badge" id="badge">Platanus Hack 26 · Bogotá · team-5</span> |
| 85 | + </header> |
| 86 | + <div class="cols"> |
| 87 | + <div class="phone"> |
| 88 | + <div class="phone-bar"> |
| 89 | + <div class="avatar">P</div> |
| 90 | + <div class="who">Parallax<small id="presence">en línea</small></div> |
| 91 | + </div> |
| 92 | + <div class="thread" id="thread"></div> |
| 93 | + </div> |
| 94 | + <div class="proof" id="proof"></div> |
| 95 | + </div> |
| 96 | + <div class="caption"><p id="caption"></p></div> |
| 97 | +</div> |
| 98 | + |
| 99 | +<script> |
| 100 | +const CONTENT = __CONTENT__; |
| 101 | +const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); |
| 102 | +// WhatsApp *bold* -> <b>, applied after escaping so content can never inject markup. |
| 103 | +const wa = (s) => esc(s).replace(/\*([^*\n]+)\*/g, "<b>$1</b>"); |
| 104 | + |
| 105 | +/** Reveal a string progressively: 0 -> "", 1 -> whole thing. */ |
| 106 | +function upto(s, p) { |
| 107 | + if (p <= 0) return ""; |
| 108 | + if (p >= 1) return s; |
| 109 | + return s.slice(0, Math.max(1, Math.floor(s.length * p))); |
| 110 | +} |
| 111 | +const span = (t, a, b) => Math.max(0, Math.min(1, (t - a) / (b - a))); |
| 112 | + |
| 113 | +const T = { |
| 114 | + healthReq: 1200, healthRes: 2600, |
| 115 | + inboundType: 5000, inboundDone: 7000, |
| 116 | + typing1: 7400, proposalStart: 8600, proposalDone: 18600, |
| 117 | + readBeat: 21200, |
| 118 | + acceptType: 21200, acceptDone: 23200, |
| 119 | + typing2: 23600, resultStart: 24600, resultDone: 28600, |
| 120 | + receiptReq: 29400, receiptRes: 31000, |
| 121 | + linkMsg: 32200, closing: 35000, end: 41000, |
| 122 | +}; |
| 123 | + |
| 124 | +const CAPTIONS = [ |
| 125 | + [0, 4600, 'No existe un ambiente de pruebas para la forma en que opera un negocio.'], |
| 126 | + [4600, 8600, 'Llega un mensaje por WhatsApp — donde ya opera América Latina.'], |
| 127 | + [8600, 19400, 'Lee el contexto y <span class="em">propone</span> un modelo con lo que realmente hay ahí. Nada ha corrido.'], |
| 128 | + [19400, 23400, 'No se activa sin unidades. Falla cerrado — no adivina.'], |
| 129 | + [23400, 29200, 'El humano acepta. Ese paso no es un trámite: es el producto.'], |
| 130 | + [29200, 34800, 'Verifica el recibo <span class="em">antes</span> de nombrarlo. No publicamos enlaces que no comprobamos.'], |
| 131 | + [34800, 41000, 'La meta no es un simulador que acierte. Es <span class="em">un simulador que no pueda mentir sobre ser un simulador.</span>'], |
| 132 | +]; |
| 133 | + |
| 134 | +function render(t) { |
| 135 | + // ---- proof panel |
| 136 | + const P = []; |
| 137 | + P.push('<h3>¿El código desplegado es el del repo?</h3>'); |
| 138 | + if (t >= T.healthReq) { |
| 139 | + P.push('<div><span class="k">GET</span> ' + esc(CONTENT.hub) + '/health</div>'); |
| 140 | + } |
| 141 | + if (t >= T.healthRes) { |
| 142 | + P.push('<div><span class="k">commit</span> <span class="hi">' + esc(CONTENT.commit) + '</span></div>'); |
| 143 | + P.push('<div class="note">`version` es una constante del código y el panel de deploy ' |
| 144 | + + 'reporta intención. El commit es el único campo que una imagen vieja no puede fingir.</div>'); |
| 145 | + } |
| 146 | + if (t >= T.receiptReq) { |
| 147 | + P.push('<h3>El recibo, comprobado antes de entregarlo</h3>'); |
| 148 | + P.push('<div><span class="k">GET</span> <span class="b">' + esc(CONTENT.receiptUrl) + '</span></div>'); |
| 149 | + } |
| 150 | + if (t >= T.receiptRes) { |
| 151 | + P.push('<div><span class="ok">HTTP ' + CONTENT.receiptStatus + '</span> · ' |
| 152 | + + CONTENT.receiptBytes + ' bytes · <span class="k">servido, no afirmado</span></div>'); |
| 153 | + } |
| 154 | + P.push('<h3>El flujo</h3>'); |
| 155 | + const STEPS = [ |
| 156 | + ['propone una ontología desde el contexto', T.proposalStart, T.acceptType], |
| 157 | + ['el humano acepta — nada corrió antes', T.acceptType, T.resultStart], |
| 158 | + ['corre el modelo hacia adelante', T.resultStart, T.receiptReq], |
| 159 | + ['recibo, comprobado antes de nombrarlo', T.receiptReq, T.end], |
| 160 | + ]; |
| 161 | + P.push('<ul class="steps">' + STEPS.map(([label, a, b]) => { |
| 162 | + const cls = t >= b ? 'done' : t >= a ? 'on' : ''; |
| 163 | + return '<li class="' + cls + '"><span class="dot"></span>' + label + '</li>'; |
| 164 | + }).join('') + '</ul>'); |
| 165 | + |
| 166 | + if (t >= T.closing) { |
| 167 | + P.push('<h3>Por qué se le puede creer</h3>'); |
| 168 | + P.push('<div class="note">La transición y las invariantes son <b>código</b>, nunca un modelo.<br>' |
| 169 | + + 'Una política <b>no puede certificarse a sí misma</b>.<br>' |
| 170 | + + 'Misma semilla → mismo hash de traza. Semilla+1 → diverge.<br>' |
| 171 | + + 'Cada valor viene tipado <b>observado | simulado</b>.</div>'); |
| 172 | + } |
| 173 | + document.getElementById('proof').innerHTML = P.join(''); |
| 174 | + |
| 175 | + // ---- thread |
| 176 | + const M = []; |
| 177 | + if (t >= T.inboundType) { |
| 178 | + M.push('<div class="msg in">' + wa(upto(CONTENT.openingText, span(t, T.inboundType, T.inboundDone))) + '</div>'); |
| 179 | + } |
| 180 | + const showTyping1 = t >= T.typing1 && t < T.proposalStart; |
| 181 | + const showTyping2 = t >= T.typing2 && t < T.resultStart; |
| 182 | + if (t >= T.proposalStart) { |
| 183 | + const full = CONTENT.proposal.join('\n\n'); |
| 184 | + const lines = full.split('\n'); |
| 185 | + const n = Math.max(1, Math.floor(lines.length * span(t, T.proposalStart, T.proposalDone))); |
| 186 | + M.push('<div class="msg out">' + wa(lines.slice(0, n).join('\n')) + '</div>'); |
| 187 | + } |
| 188 | + if (t >= T.acceptType) { |
| 189 | + const flat = CONTENT.acceptText.split('\n').join(' '); |
| 190 | + M.push('<div class="msg in">' + wa(upto(flat, span(t, T.acceptType, T.acceptDone))) + '</div>'); |
| 191 | + } |
| 192 | + if (t >= T.resultStart) { |
| 193 | + const full = CONTENT.result.join('\n\n').replace(/\s*Receipt:.*$/s, ''); |
| 194 | + const lines = full.split('\n'); |
| 195 | + const n = Math.max(1, Math.floor(lines.length * span(t, T.resultStart, T.resultDone))); |
| 196 | + M.push('<div class="msg out">' + wa(lines.slice(0, n).join('\n')) + '</div>'); |
| 197 | + } |
| 198 | + if (t >= T.linkMsg) { |
| 199 | + M.push('<div class="msg out">listo. el recibo:\n' + esc(CONTENT.receiptUrl) + '</div>'); |
| 200 | + } |
| 201 | + if (showTyping1 || showTyping2) { |
| 202 | + M.push('<div class="typing"><i></i><i></i><i></i></div>'); |
| 203 | + } |
| 204 | + document.getElementById('thread').innerHTML = M.join(''); |
| 205 | + document.getElementById('presence').textContent = |
| 206 | + (showTyping1 || showTyping2) ? 'escribiendo…' : 'en línea'; |
| 207 | + |
| 208 | + // ---- caption |
| 209 | + const c = CAPTIONS.find(([a, b]) => t >= a && t < b); |
| 210 | + document.getElementById('caption').innerHTML = c ? c[2] : ''; |
| 211 | + return true; |
| 212 | +} |
| 213 | +render(0); |
| 214 | +</script> |
0 commit comments